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