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

Xls::save()   F

Complexity

Conditions 16
Paths 3584

Size

Total Lines 114
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 59
CRAP Score 16

Importance

Changes 0
Metric Value
eloc 58
dl 0
loc 114
ccs 59
cts 59
cp 1
rs 1.4
c 0
b 0
f 0
cc 16
nc 3584
nop 2
crap 16

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\RichText\RichText;
10
use PhpOffice\PhpSpreadsheet\RichText\Run;
11
use PhpOffice\PhpSpreadsheet\Shared\Escher;
12
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
13
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
14
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
15
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
17
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
18
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
19
use PhpOffice\PhpSpreadsheet\Shared\OLE;
20
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File;
21
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
22
use PhpOffice\PhpSpreadsheet\Spreadsheet;
23
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
24
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
25
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
26
use PhpOffice\PhpSpreadsheet\Writer\Xls\Parser;
27
use PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook;
28
use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet;
29
30
class Xls extends BaseWriter
31
{
32
    /**
33
     * PhpSpreadsheet object.
34
     *
35
     * @var Spreadsheet
36
     */
37
    private $spreadsheet;
38
39
    /**
40
     * Total number of shared strings in workbook.
41
     *
42
     * @var int
43
     */
44
    private $strTotal = 0;
45
46
    /**
47
     * Number of unique shared strings in workbook.
48
     *
49
     * @var int
50
     */
51
    private $strUnique = 0;
52
53
    /**
54
     * Array of unique shared strings in workbook.
55
     *
56
     * @var array
57
     */
58
    private $strTable = [];
59
60
    /**
61
     * Color cache. Mapping between RGB value and color index.
62
     *
63
     * @var array
64
     */
65
    private $colors;
66
67
    /**
68
     * Formula parser.
69
     *
70
     * @var Parser
71
     */
72
    private $parser;
73
74
    /**
75
     * Identifier clusters for drawings. Used in MSODRAWINGGROUP record.
76
     *
77
     * @var array
78
     */
79
    private $IDCLs;
80
81
    /**
82
     * Basic OLE object summary information.
83
     *
84
     * @var string
85
     */
86
    private $summaryInformation;
87
88
    /**
89
     * Extended OLE object document summary information.
90
     *
91
     * @var string
92
     */
93
    private $documentSummaryInformation;
94
95
    /**
96
     * @var Workbook
97
     */
98
    private $writerWorkbook;
99
100
    /**
101
     * @var Worksheet[]
102
     */
103
    private $writerWorksheets;
104
105
    /**
106
     * Create a new Xls Writer.
107
     *
108
     * @param Spreadsheet $spreadsheet PhpSpreadsheet object
109
     */
110 89
    public function __construct(Spreadsheet $spreadsheet)
111
    {
112 89
        $this->spreadsheet = $spreadsheet;
113
114 89
        $this->parser = new Xls\Parser($spreadsheet);
115
    }
116
117
    /**
118
     * Save Spreadsheet to file.
119
     *
120
     * @param resource|string $filename
121
     */
122 88
    public function save($filename, int $flags = 0): void
123
    {
124 88
        $this->processFlags($flags);
125
126
        // garbage collect
127 88
        $this->spreadsheet->garbageCollect();
128
129 88
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
130 88
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
131 88
        $saveDateReturnType = Functions::getReturnDateType();
132 88
        Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
133
134
        // initialize colors array
135 88
        $this->colors = [];
136
137
        // Initialise workbook writer
138 88
        $this->writerWorkbook = new Xls\Workbook($this->spreadsheet, $this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser);
139
140
        // Initialise worksheet writers
141 88
        $countSheets = $this->spreadsheet->getSheetCount();
142 88
        for ($i = 0; $i < $countSheets; ++$i) {
143 88
            $this->writerWorksheets[$i] = new Xls\Worksheet($this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser, $this->preCalculateFormulas, $this->spreadsheet->getSheet($i));
144
        }
145
146
        // build Escher objects. Escher objects for workbooks needs to be build before Escher object for workbook.
147 88
        $this->buildWorksheetEschers();
148 88
        $this->buildWorkbookEscher();
149
150
        // add 15 identical cell style Xfs
151
        // for now, we use the first cellXf instead of cellStyleXf
152 88
        $cellXfCollection = $this->spreadsheet->getCellXfCollection();
153 88
        for ($i = 0; $i < 15; ++$i) {
154 88
            $this->writerWorkbook->addXfWriter($cellXfCollection[0], true);
155
        }
156
157
        // add all the cell Xfs
158 88
        foreach ($this->spreadsheet->getCellXfCollection() as $style) {
159 88
            $this->writerWorkbook->addXfWriter($style, false);
160
        }
161
162
        // add fonts from rich text eleemnts
163 88
        for ($i = 0; $i < $countSheets; ++$i) {
164 88
            foreach ($this->writerWorksheets[$i]->phpSheet->getCellCollection()->getCoordinates() as $coordinate) {
165
                /** @var Cell $cell */
166 84
                $cell = $this->writerWorksheets[$i]->phpSheet->getCellCollection()->get($coordinate);
167 84
                $cVal = $cell->getValue();
168 84
                if ($cVal instanceof RichText) {
169 11
                    $elements = $cVal->getRichTextElements();
170 11
                    foreach ($elements as $element) {
171 11
                        if ($element instanceof Run) {
172 11
                            $font = $element->getFont();
173 11
                            if ($font !== null) {
174 11
                                $this->writerWorksheets[$i]->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font);
175
                            }
176
                        }
177
                    }
178
                }
179
            }
180
        }
181
182
        // initialize OLE file
183 88
        $workbookStreamName = 'Workbook';
184 88
        $OLE = new File(OLE::ascToUcs($workbookStreamName));
185
186
        // Write the worksheet streams before the global workbook stream,
187
        // because the byte sizes of these are needed in the global workbook stream
188 88
        $worksheetSizes = [];
189 88
        for ($i = 0; $i < $countSheets; ++$i) {
190 88
            $this->writerWorksheets[$i]->close();
191 88
            $worksheetSizes[] = $this->writerWorksheets[$i]->_datasize;
192
        }
193
194
        // add binary data for global workbook stream
195 88
        $OLE->append($this->writerWorkbook->writeWorkbook($worksheetSizes));
196
197
        // add binary data for sheet streams
198 88
        for ($i = 0; $i < $countSheets; ++$i) {
199 88
            $OLE->append($this->writerWorksheets[$i]->getData());
200
        }
201
202 88
        $this->documentSummaryInformation = $this->writeDocumentSummaryInformation();
203
        // initialize OLE Document Summary Information
204 88
        if (!empty($this->documentSummaryInformation)) {
205 88
            $OLE_DocumentSummaryInformation = new File(OLE::ascToUcs(chr(5) . 'DocumentSummaryInformation'));
206 88
            $OLE_DocumentSummaryInformation->append($this->documentSummaryInformation);
207
        }
208
209 88
        $this->summaryInformation = $this->writeSummaryInformation();
210
        // initialize OLE Summary Information
211 88
        if (!empty($this->summaryInformation)) {
212 88
            $OLE_SummaryInformation = new File(OLE::ascToUcs(chr(5) . 'SummaryInformation'));
213 88
            $OLE_SummaryInformation->append($this->summaryInformation);
214
        }
215
216
        // define OLE Parts
217 88
        $arrRootData = [$OLE];
218
        // initialize OLE Properties file
219 88
        if (isset($OLE_SummaryInformation)) {
220 88
            $arrRootData[] = $OLE_SummaryInformation;
221
        }
222
        // initialize OLE Extended Properties file
223 88
        if (isset($OLE_DocumentSummaryInformation)) {
224 88
            $arrRootData[] = $OLE_DocumentSummaryInformation;
225
        }
226
227 88
        $time = $this->spreadsheet->getProperties()->getModified();
228 88
        $root = new Root($time, $time, $arrRootData);
229
        // save the OLE file
230 88
        $this->openFileHandle($filename);
231 88
        $root->save($this->fileHandle);
232 88
        $this->maybeCloseFileHandle();
233
234 88
        Functions::setReturnDateType($saveDateReturnType);
235 88
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
236
    }
237
238
    /**
239
     * Build the Worksheet Escher objects.
240
     */
241 88
    private function buildWorksheetEschers(): void
242
    {
243
        // 1-based index to BstoreContainer
244 88
        $blipIndex = 0;
245 88
        $lastReducedSpId = 0;
246 88
        $lastSpId = 0;
247
248 88
        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
249
            // sheet index
250 88
            $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
251
252
            // check if there are any shapes for this sheet
253 88
            $filterRange = $sheet->getAutoFilter()->getRange();
254 88
            if (count($sheet->getDrawingCollection()) == 0 && empty($filterRange)) {
255 74
                continue;
256
            }
257
258
            // create intermediate Escher object
259 15
            $escher = new Escher();
260
261
            // dgContainer
262 15
            $dgContainer = new DgContainer();
263
264
            // set the drawing index (we use sheet index + 1)
265 15
            $dgId = $sheet->getParentOrThrow()->getIndex($sheet) + 1;
266 15
            $dgContainer->setDgId($dgId);
267 15
            $escher->setDgContainer($dgContainer);
268
269
            // spgrContainer
270 15
            $spgrContainer = new SpgrContainer();
271 15
            $dgContainer->setSpgrContainer($spgrContainer);
272
273
            // add one shape which is the group shape
274 15
            $spContainer = new SpContainer();
275 15
            $spContainer->setSpgr(true);
276 15
            $spContainer->setSpType(0);
277 15
            $spContainer->setSpId(($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10);
278 15
            $spgrContainer->addChild($spContainer);
279
280
            // add the shapes
281
282 15
            $countShapes[$sheetIndex] = 0; // count number of shapes (minus group shape), in sheet
283
284 15
            foreach ($sheet->getDrawingCollection() as $drawing) {
285 11
                ++$blipIndex;
286
287 11
                ++$countShapes[$sheetIndex];
288
289
                // add the shape
290 11
                $spContainer = new SpContainer();
291
292
                // set the shape type
293 11
                $spContainer->setSpType(0x004B);
294
                // set the shape flag
295 11
                $spContainer->setSpFlag(0x02);
296
297
                // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
298 11
                $reducedSpId = $countShapes[$sheetIndex];
299 11
                $spId = $reducedSpId | ($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10;
300 11
                $spContainer->setSpId($spId);
301
302
                // keep track of last reducedSpId
303 11
                $lastReducedSpId = $reducedSpId;
304
305
                // keep track of last spId
306 11
                $lastSpId = $spId;
307
308
                // set the BLIP index
309 11
                $spContainer->setOPT(0x4104, $blipIndex);
310
311
                // set coordinates and offsets, client anchor
312 11
                $coordinates = $drawing->getCoordinates();
313 11
                $offsetX = $drawing->getOffsetX();
314 11
                $offsetY = $drawing->getOffsetY();
315 11
                $width = $drawing->getWidth();
316 11
                $height = $drawing->getHeight();
317
318 11
                $twoAnchor = \PhpOffice\PhpSpreadsheet\Shared\Xls::oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height);
319
320 11
                if (is_array($twoAnchor)) {
321 11
                    $spContainer->setStartCoordinates($twoAnchor['startCoordinates']);
322 11
                    $spContainer->setStartOffsetX($twoAnchor['startOffsetX']);
323 11
                    $spContainer->setStartOffsetY($twoAnchor['startOffsetY']);
324 11
                    $spContainer->setEndCoordinates($twoAnchor['endCoordinates']);
325 11
                    $spContainer->setEndOffsetX($twoAnchor['endOffsetX']);
326 11
                    $spContainer->setEndOffsetY($twoAnchor['endOffsetY']);
327
328 11
                    $spgrContainer->addChild($spContainer);
329
                }
330
            }
331
332
            // AutoFilters
333 15
            if (!empty($filterRange)) {
334 4
                $rangeBounds = Coordinate::rangeBoundaries($filterRange);
335 4
                $iNumColStart = $rangeBounds[0][0];
336 4
                $iNumColEnd = $rangeBounds[1][0];
337
338 4
                $iInc = $iNumColStart;
339 4
                while ($iInc <= $iNumColEnd) {
340 4
                    ++$countShapes[$sheetIndex];
341
342
                    // create an Drawing Object for the dropdown
343 4
                    $oDrawing = new BaseDrawing();
344
                    // get the coordinates of drawing
345 4
                    $cDrawing = Coordinate::stringFromColumnIndex($iInc) . $rangeBounds[0][1];
346 4
                    $oDrawing->setCoordinates($cDrawing);
347 4
                    $oDrawing->setWorksheet($sheet);
348
349
                    // add the shape
350 4
                    $spContainer = new SpContainer();
351
                    // set the shape type
352 4
                    $spContainer->setSpType(0x00C9);
353
                    // set the shape flag
354 4
                    $spContainer->setSpFlag(0x01);
355
356
                    // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
357 4
                    $reducedSpId = $countShapes[$sheetIndex];
358 4
                    $spId = $reducedSpId | ($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10;
359 4
                    $spContainer->setSpId($spId);
360
361
                    // keep track of last reducedSpId
362 4
                    $lastReducedSpId = $reducedSpId;
363
364
                    // keep track of last spId
365 4
                    $lastSpId = $spId;
366
367 4
                    $spContainer->setOPT(0x007F, 0x01040104); // Protection -> fLockAgainstGrouping
368 4
                    $spContainer->setOPT(0x00BF, 0x00080008); // Text -> fFitTextToShape
369 4
                    $spContainer->setOPT(0x01BF, 0x00010000); // Fill Style -> fNoFillHitTest
370 4
                    $spContainer->setOPT(0x01FF, 0x00080000); // Line Style -> fNoLineDrawDash
371 4
                    $spContainer->setOPT(0x03BF, 0x000A0000); // Group Shape -> fPrint
372
373
                    // set coordinates and offsets, client anchor
374 4
                    $endCoordinates = Coordinate::stringFromColumnIndex($iInc);
375 4
                    $endCoordinates .= $rangeBounds[0][1] + 1;
376
377 4
                    $spContainer->setStartCoordinates($cDrawing);
378 4
                    $spContainer->setStartOffsetX(0);
379 4
                    $spContainer->setStartOffsetY(0);
380 4
                    $spContainer->setEndCoordinates($endCoordinates);
381 4
                    $spContainer->setEndOffsetX(0);
382 4
                    $spContainer->setEndOffsetY(0);
383
384 4
                    $spgrContainer->addChild($spContainer);
385 4
                    ++$iInc;
386
                }
387
            }
388
389
            // identifier clusters, used for workbook Escher object
390 15
            $this->IDCLs[$dgId] = $lastReducedSpId;
391
392
            // set last shape index
393 15
            $dgContainer->setLastSpId($lastSpId);
394
395
            // set the Escher object
396 15
            $this->writerWorksheets[$sheetIndex]->setEscher($escher);
397
        }
398
    }
399
400 3
    private function processMemoryDrawing(BstoreContainer &$bstoreContainer, MemoryDrawing $drawing, string $renderingFunctionx): void
401
    {
402
        switch ($renderingFunctionx) {
403 1
            case MemoryDrawing::RENDERING_JPEG:
404 3
                $blipType = BSE::BLIPTYPE_JPEG;
405 3
                $renderingFunction = 'imagejpeg';
406
407 3
                break;
408
            default:
409 3
                $blipType = BSE::BLIPTYPE_PNG;
410 3
                $renderingFunction = 'imagepng';
411
412 3
                break;
413
        }
414
415 3
        ob_start();
416 3
        call_user_func($renderingFunction, $drawing->getImageResource());
417 3
        $blipData = ob_get_contents();
418 3
        ob_end_clean();
419
420 3
        $blip = new Blip();
421 3
        $blip->setData("$blipData");
422
423 3
        $BSE = new BSE();
424 3
        $BSE->setBlipType($blipType);
425 3
        $BSE->setBlip($blip);
426
427 3
        $bstoreContainer->addBSE($BSE);
428
    }
429
430 9
    private function processDrawing(BstoreContainer &$bstoreContainer, Drawing $drawing): void
431
    {
432 9
        $blipType = 0;
433 9
        $blipData = '';
434 9
        $filename = $drawing->getPath();
435
436 9
        $imageSize = getimagesize($filename);
437 9
        $imageFormat = empty($imageSize) ? 0 : ($imageSize[2] ?? 0);
438
439
        switch ($imageFormat) {
440 9
            case 1: // GIF, not supported by BIFF8, we convert to PNG
441 2
                $blipType = BSE::BLIPTYPE_PNG;
442 2
                $newImage = @imagecreatefromgif($filename);
443 2
                if ($newImage === false) {
444
                    throw new Exception("Unable to create image from $filename");
445
                }
446 2
                ob_start();
447 2
                imagepng($newImage);
448 2
                $blipData = ob_get_contents();
449 2
                ob_end_clean();
450
451 2
                break;
452 8
            case 2: // JPEG
453 7
                $blipType = BSE::BLIPTYPE_JPEG;
454 7
                $blipData = file_get_contents($filename);
455
456 7
                break;
457 8
            case 3: // PNG
458 7
                $blipType = BSE::BLIPTYPE_PNG;
459 7
                $blipData = file_get_contents($filename);
460
461 7
                break;
462 2
            case 6: // Windows DIB (BMP), we convert to PNG
463 2
                $blipType = BSE::BLIPTYPE_PNG;
464 2
                $newImage = @imagecreatefrombmp($filename);
465 2
                if ($newImage === false) {
466
                    throw new Exception("Unable to create image from $filename");
467
                }
468 2
                ob_start();
469 2
                imagepng($newImage);
470 2
                $blipData = ob_get_contents();
471 2
                ob_end_clean();
472
473 2
                break;
474
        }
475 9
        if ($blipData) {
476 9
            $blip = new Blip();
477 9
            $blip->setData($blipData);
478
479 9
            $BSE = new BSE();
480 9
            $BSE->setBlipType($blipType);
481 9
            $BSE->setBlip($blip);
482
483 9
            $bstoreContainer->addBSE($BSE);
484
        }
485
    }
486
487 15
    private function processBaseDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void
488
    {
489 15
        if ($drawing instanceof Drawing) {
490 9
            $this->processDrawing($bstoreContainer, $drawing);
491 7
        } elseif ($drawing instanceof MemoryDrawing) {
492 3
            $this->processMemoryDrawing($bstoreContainer, $drawing, $drawing->getRenderingFunction());
493
        }
494
    }
495
496 88
    private function checkForDrawings(): bool
497
    {
498
        // any drawings in this workbook?
499 88
        $found = false;
500 88
        foreach ($this->spreadsheet->getAllSheets() as $sheet) {
501 88
            if (count($sheet->getDrawingCollection()) > 0) {
502 15
                $found = true;
503
504 15
                break;
505
            }
506
        }
507
508 88
        return $found;
509
    }
510
511
    /**
512
     * Build the Escher object corresponding to the MSODRAWINGGROUP record.
513
     */
514 88
    private function buildWorkbookEscher(): void
515
    {
516
        // nothing to do if there are no drawings
517 88
        if (!$this->checkForDrawings()) {
518 73
            return;
519
        }
520
521
        // if we reach here, then there are drawings in the workbook
522 15
        $escher = new Escher();
523
524
        // dggContainer
525 15
        $dggContainer = new DggContainer();
526 15
        $escher->setDggContainer($dggContainer);
527
528
        // set IDCLs (identifier clusters)
529 15
        $dggContainer->setIDCLs($this->IDCLs);
530
531
        // this loop is for determining maximum shape identifier of all drawing
532 15
        $spIdMax = 0;
533 15
        $totalCountShapes = 0;
534 15
        $countDrawings = 0;
535
536 15
        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
537 15
            $sheetCountShapes = 0; // count number of shapes (minus group shape), in sheet
538
539 15
            $addCount = 0;
540 15
            foreach ($sheet->getDrawingCollection() as $drawing) {
541 15
                $addCount = 1;
542 15
                ++$sheetCountShapes;
543 15
                ++$totalCountShapes;
544
545 15
                $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10;
546 15
                $spIdMax = max($spId, $spIdMax);
547
            }
548 15
            $countDrawings += $addCount;
549
        }
550
551 15
        $dggContainer->setSpIdMax($spIdMax + 1);
552 15
        $dggContainer->setCDgSaved($countDrawings);
553 15
        $dggContainer->setCSpSaved($totalCountShapes + $countDrawings); // total number of shapes incl. one group shapes per drawing
554
555
        // bstoreContainer
556 15
        $bstoreContainer = new BstoreContainer();
557 15
        $dggContainer->setBstoreContainer($bstoreContainer);
558
559
        // the BSE's (all the images)
560 15
        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
561 15
            foreach ($sheet->getDrawingCollection() as $drawing) {
562 15
                $this->processBaseDrawing($bstoreContainer, $drawing);
563
            }
564
        }
565
566
        // Set the Escher object
567 15
        $this->writerWorkbook->setEscher($escher);
568
    }
569
570
    /**
571
     * Build the OLE Part for DocumentSummary Information.
572
     *
573
     * @return string
574
     */
575 88
    private function writeDocumentSummaryInformation()
576
    {
577
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
578 88
        $data = pack('v', 0xFFFE);
579
        // offset: 2; size: 2;
580 88
        $data .= pack('v', 0x0000);
581
        // offset: 4; size: 2; OS version
582 88
        $data .= pack('v', 0x0106);
583
        // offset: 6; size: 2; OS indicator
584 88
        $data .= pack('v', 0x0002);
585
        // offset: 8; size: 16
586 88
        $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
587
        // offset: 24; size: 4; section count
588 88
        $data .= pack('V', 0x0001);
589
590
        // offset: 28; size: 16; first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
591 88
        $data .= pack('vvvvvvvv', 0xD502, 0xD5CD, 0x2E9C, 0x101B, 0x9793, 0x0008, 0x2C2B, 0xAEF9);
592
        // offset: 44; size: 4; offset of the start
593 88
        $data .= pack('V', 0x30);
594
595
        // SECTION
596 88
        $dataSection = [];
597 88
        $dataSection_NumProps = 0;
598 88
        $dataSection_Summary = '';
599 88
        $dataSection_Content = '';
600
601
        // GKPIDDSI_CODEPAGE: CodePage
602 88
        $dataSection[] = [
603 88
            'summary' => ['pack' => 'V', 'data' => 0x01],
604 88
            'offset' => ['pack' => 'V'],
605 88
            'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
606 88
            'data' => ['data' => 1252],
607 88
        ];
608 88
        ++$dataSection_NumProps;
609
610
        // GKPIDDSI_CATEGORY : Category
611 88
        $dataProp = $this->spreadsheet->getProperties()->getCategory();
612 88
        if ($dataProp) {
613 41
            $dataSection[] = [
614 41
                'summary' => ['pack' => 'V', 'data' => 0x02],
615 41
                'offset' => ['pack' => 'V'],
616 41
                'type' => ['pack' => 'V', 'data' => 0x1E],
617 41
                'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
618 41
            ];
619 41
            ++$dataSection_NumProps;
620
        }
621
        // GKPIDDSI_VERSION :Version of the application that wrote the property storage
622 88
        $dataSection[] = [
623 88
            'summary' => ['pack' => 'V', 'data' => 0x17],
624 88
            'offset' => ['pack' => 'V'],
625 88
            'type' => ['pack' => 'V', 'data' => 0x03],
626 88
            'data' => ['pack' => 'V', 'data' => 0x000C0000],
627 88
        ];
628 88
        ++$dataSection_NumProps;
629
        // GKPIDDSI_SCALE : FALSE
630 88
        $dataSection[] = [
631 88
            'summary' => ['pack' => 'V', 'data' => 0x0B],
632 88
            'offset' => ['pack' => 'V'],
633 88
            'type' => ['pack' => 'V', 'data' => 0x0B],
634 88
            'data' => ['data' => false],
635 88
        ];
636 88
        ++$dataSection_NumProps;
637
        // GKPIDDSI_LINKSDIRTY : True if any of the values for the linked properties have changed outside of the application
638 88
        $dataSection[] = [
639 88
            'summary' => ['pack' => 'V', 'data' => 0x10],
640 88
            'offset' => ['pack' => 'V'],
641 88
            'type' => ['pack' => 'V', 'data' => 0x0B],
642 88
            'data' => ['data' => false],
643 88
        ];
644 88
        ++$dataSection_NumProps;
645
        // GKPIDDSI_SHAREDOC : FALSE
646 88
        $dataSection[] = [
647 88
            'summary' => ['pack' => 'V', 'data' => 0x13],
648 88
            'offset' => ['pack' => 'V'],
649 88
            'type' => ['pack' => 'V', 'data' => 0x0B],
650 88
            'data' => ['data' => false],
651 88
        ];
652 88
        ++$dataSection_NumProps;
653
        // GKPIDDSI_HYPERLINKSCHANGED : True if any of the values for the _PID_LINKS (hyperlink text) have changed outside of the application
654 88
        $dataSection[] = [
655 88
            'summary' => ['pack' => 'V', 'data' => 0x16],
656 88
            'offset' => ['pack' => 'V'],
657 88
            'type' => ['pack' => 'V', 'data' => 0x0B],
658 88
            'data' => ['data' => false],
659 88
        ];
660 88
        ++$dataSection_NumProps;
661
662
        // GKPIDDSI_DOCSPARTS
663
        // MS-OSHARED p75 (2.3.3.2.2.1)
664
        // Structure is VtVecUnalignedLpstrValue (2.3.3.1.9)
665
        // cElements
666 88
        $dataProp = pack('v', 0x0001);
667 88
        $dataProp .= pack('v', 0x0000);
668
        // array of UnalignedLpstr
669
        // cch
670 88
        $dataProp .= pack('v', 0x000A);
671 88
        $dataProp .= pack('v', 0x0000);
672
        // value
673 88
        $dataProp .= 'Worksheet' . chr(0);
674
675 88
        $dataSection[] = [
676 88
            'summary' => ['pack' => 'V', 'data' => 0x0D],
677 88
            'offset' => ['pack' => 'V'],
678 88
            'type' => ['pack' => 'V', 'data' => 0x101E],
679 88
            'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
680 88
        ];
681 88
        ++$dataSection_NumProps;
682
683
        // GKPIDDSI_HEADINGPAIR
684
        // VtVecHeadingPairValue
685
        // cElements
686 88
        $dataProp = pack('v', 0x0002);
687 88
        $dataProp .= pack('v', 0x0000);
688
        // Array of vtHeadingPair
689
        // vtUnalignedString - headingString
690
        // stringType
691 88
        $dataProp .= pack('v', 0x001E);
692
        // padding
693 88
        $dataProp .= pack('v', 0x0000);
694
        // UnalignedLpstr
695
        // cch
696 88
        $dataProp .= pack('v', 0x0013);
697 88
        $dataProp .= pack('v', 0x0000);
698
        // value
699 88
        $dataProp .= 'Feuilles de calcul';
700
        // vtUnalignedString - headingParts
701
        // wType : 0x0003 = 32 bit signed integer
702 88
        $dataProp .= pack('v', 0x0300);
703
        // padding
704 88
        $dataProp .= pack('v', 0x0000);
705
        // value
706 88
        $dataProp .= pack('v', 0x0100);
707 88
        $dataProp .= pack('v', 0x0000);
708 88
        $dataProp .= pack('v', 0x0000);
709 88
        $dataProp .= pack('v', 0x0000);
710
711 88
        $dataSection[] = [
712 88
            'summary' => ['pack' => 'V', 'data' => 0x0C],
713 88
            'offset' => ['pack' => 'V'],
714 88
            'type' => ['pack' => 'V', 'data' => 0x100C],
715 88
            'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
716 88
        ];
717 88
        ++$dataSection_NumProps;
718
719
        //         4     Section Length
720
        //        4     Property count
721
        //        8 * $dataSection_NumProps (8 =  ID (4) + OffSet(4))
722 88
        $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
723 88
        foreach ($dataSection as $dataProp) {
724
            // Summary
725 88
            $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
726
            // Offset
727 88
            $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
728
            // DataType
729 88
            $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
730
            // Data
731 88
            if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
732 88
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
733
734 88
                $dataSection_Content_Offset += 4 + 4;
735 88
            } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
736 88
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
737
738 88
                $dataSection_Content_Offset += 4 + 4;
739 88
            } elseif ($dataProp['type']['data'] == 0x0B) { // Boolean
740 88
                $dataSection_Content .= pack('V', (int) $dataProp['data']['data']);
741 88
                $dataSection_Content_Offset += 4 + 4;
742 88
            } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
743
                // Null-terminated string
744 41
                $dataProp['data']['data'] .= chr(0);
745 41
                ++$dataProp['data']['length'];
746
                // Complete the string with null string for being a %4
747 41
                $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
748 41
                $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
749
750 41
                $dataSection_Content .= pack('V', $dataProp['data']['length']);
751 41
                $dataSection_Content .= $dataProp['data']['data'];
752
753 41
                $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
754
            // Condition below can never be true
755
            //} elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
756
            //    $dataSection_Content .= $dataProp['data']['data'];
757
758
            //    $dataSection_Content_Offset += 4 + 8;
759
            } else {
760 88
                $dataSection_Content .= $dataProp['data']['data'];
761
762 88
                $dataSection_Content_Offset += 4 + $dataProp['data']['length'];
763
            }
764
        }
765
        // Now $dataSection_Content_Offset contains the size of the content
766
767
        // section header
768
        // offset: $secOffset; size: 4; section length
769
        //         + x  Size of the content (summary + content)
770 88
        $data .= pack('V', $dataSection_Content_Offset);
771
        // offset: $secOffset+4; size: 4; property count
772 88
        $data .= pack('V', $dataSection_NumProps);
773
        // Section Summary
774 88
        $data .= $dataSection_Summary;
775
        // Section Content
776 88
        $data .= $dataSection_Content;
777
778 88
        return $data;
779
    }
780
781
    /**
782
     * @param float|int $dataProp
783
     */
784 88
    private function writeSummaryPropOle($dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
785
    {
786 88
        if ($dataProp) {
787 88
            $dataSection[] = [
788 88
                'summary' => ['pack' => 'V', 'data' => $sumdata],
789 88
                'offset' => ['pack' => 'V'],
790 88
                'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
791 88
                'data' => ['data' => OLE::localDateToOLE($dataProp)],
792 88
            ];
793 88
            ++$dataSection_NumProps;
794
        }
795
    }
796
797 88
    private function writeSummaryProp(string $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
798
    {
799 88
        if ($dataProp) {
800 88
            $dataSection[] = [
801 88
                'summary' => ['pack' => 'V', 'data' => $sumdata],
802 88
                'offset' => ['pack' => 'V'],
803 88
                'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
804 88
                'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
805 88
            ];
806 88
            ++$dataSection_NumProps;
807
        }
808
    }
809
810
    /**
811
     * Build the OLE Part for Summary Information.
812
     *
813
     * @return string
814
     */
815 88
    private function writeSummaryInformation()
816
    {
817
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
818 88
        $data = pack('v', 0xFFFE);
819
        // offset: 2; size: 2;
820 88
        $data .= pack('v', 0x0000);
821
        // offset: 4; size: 2; OS version
822 88
        $data .= pack('v', 0x0106);
823
        // offset: 6; size: 2; OS indicator
824 88
        $data .= pack('v', 0x0002);
825
        // offset: 8; size: 16
826 88
        $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
827
        // offset: 24; size: 4; section count
828 88
        $data .= pack('V', 0x0001);
829
830
        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
831 88
        $data .= pack('vvvvvvvv', 0x85E0, 0xF29F, 0x4FF9, 0x1068, 0x91AB, 0x0008, 0x272B, 0xD9B3);
832
        // offset: 44; size: 4; offset of the start
833 88
        $data .= pack('V', 0x30);
834
835
        // SECTION
836 88
        $dataSection = [];
837 88
        $dataSection_NumProps = 0;
838 88
        $dataSection_Summary = '';
839 88
        $dataSection_Content = '';
840
841
        // CodePage : CP-1252
842 88
        $dataSection[] = [
843 88
            'summary' => ['pack' => 'V', 'data' => 0x01],
844 88
            'offset' => ['pack' => 'V'],
845 88
            'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
846 88
            'data' => ['data' => 1252],
847 88
        ];
848 88
        ++$dataSection_NumProps;
849
850 88
        $props = $this->spreadsheet->getProperties();
851 88
        $this->writeSummaryProp($props->getTitle(), $dataSection_NumProps, $dataSection, 0x02, 0x1e);
852 88
        $this->writeSummaryProp($props->getSubject(), $dataSection_NumProps, $dataSection, 0x03, 0x1e);
853 88
        $this->writeSummaryProp($props->getCreator(), $dataSection_NumProps, $dataSection, 0x04, 0x1e);
854 88
        $this->writeSummaryProp($props->getKeywords(), $dataSection_NumProps, $dataSection, 0x05, 0x1e);
855 88
        $this->writeSummaryProp($props->getDescription(), $dataSection_NumProps, $dataSection, 0x06, 0x1e);
856 88
        $this->writeSummaryProp($props->getLastModifiedBy(), $dataSection_NumProps, $dataSection, 0x08, 0x1e);
857 88
        $this->writeSummaryPropOle($props->getCreated(), $dataSection_NumProps, $dataSection, 0x0c, 0x40);
858 88
        $this->writeSummaryPropOle($props->getModified(), $dataSection_NumProps, $dataSection, 0x0d, 0x40);
859
860
        //    Security
861 88
        $dataSection[] = [
862 88
            'summary' => ['pack' => 'V', 'data' => 0x13],
863 88
            'offset' => ['pack' => 'V'],
864 88
            'type' => ['pack' => 'V', 'data' => 0x03], // 4 byte signed integer
865 88
            'data' => ['data' => 0x00],
866 88
        ];
867 88
        ++$dataSection_NumProps;
868
869
        //         4     Section Length
870
        //        4     Property count
871
        //        8 * $dataSection_NumProps (8 =  ID (4) + OffSet(4))
872 88
        $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
873 88
        foreach ($dataSection as $dataProp) {
874
            // Summary
875 88
            $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
876
            // Offset
877 88
            $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
878
            // DataType
879 88
            $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
880
            // Data
881 88
            if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
882 88
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
883
884 88
                $dataSection_Content_Offset += 4 + 4;
885 88
            } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
886 88
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
887
888 88
                $dataSection_Content_Offset += 4 + 4;
889 88
            } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
890
                // Null-terminated string
891 88
                $dataProp['data']['data'] .= chr(0);
892 88
                ++$dataProp['data']['length'];
893
                // Complete the string with null string for being a %4
894 88
                $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
895 88
                $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
896
897 88
                $dataSection_Content .= pack('V', $dataProp['data']['length']);
898 88
                $dataSection_Content .= $dataProp['data']['data'];
899
900 88
                $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
901 88
            } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
902 88
                $dataSection_Content .= $dataProp['data']['data'];
903
904 88
                $dataSection_Content_Offset += 4 + 8;
905
            }
906
            // Data Type Not Used at the moment
907
        }
908
        // Now $dataSection_Content_Offset contains the size of the content
909
910
        // section header
911
        // offset: $secOffset; size: 4; section length
912
        //         + x  Size of the content (summary + content)
913 88
        $data .= pack('V', $dataSection_Content_Offset);
914
        // offset: $secOffset+4; size: 4; property count
915 88
        $data .= pack('V', $dataSection_NumProps);
916
        // Section Summary
917 88
        $data .= $dataSection_Summary;
918
        // Section Content
919 88
        $data .= $dataSection_Content;
920
921 88
        return $data;
922
    }
923
}
924