1<?php
2
3namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
7use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
8use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
9use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
10use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
11use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
12use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
13
14class Escher
15{
16    /**
17     * The object we are writing.
18     */
19    private $object;
20
21    /**
22     * The written binary data.
23     */
24    private $data;
25
26    /**
27     * Shape offsets. Positions in binary stream where a new shape record begins.
28     *
29     * @var array
30     */
31    private $spOffsets;
32
33    /**
34     * Shape types.
35     *
36     * @var array
37     */
38    private $spTypes;
39
40    /**
41     * Constructor.
42     *
43     * @param mixed $object
44     */
45    public function __construct($object)
46    {
47        $this->object = $object;
48    }
49
50    /**
51     * Process the object to be written.
52     *
53     * @return string
54     */
55    public function close()
56    {
57        // initialize
58        $this->data = '';
59
60        switch (get_class($this->object)) {
61            case \PhpOffice\PhpSpreadsheet\Shared\Escher::class:
62                if ($dggContainer = $this->object->getDggContainer()) {
63                    $writer = new self($dggContainer);
64                    $this->data = $writer->close();
65                } elseif ($dgContainer = $this->object->getDgContainer()) {
66                    $writer = new self($dgContainer);
67                    $this->data = $writer->close();
68                    $this->spOffsets = $writer->getSpOffsets();
69                    $this->spTypes = $writer->getSpTypes();
70                }
71
72                break;
73            case DggContainer::class:
74                // this is a container record
75
76                // initialize
77                $innerData = '';
78
79                // write the dgg
80                $recVer = 0x0;
81                $recInstance = 0x0000;
82                $recType = 0xF006;
83
84                $recVerInstance = $recVer;
85                $recVerInstance |= $recInstance << 4;
86
87                // dgg data
88                $dggData =
89                    pack(
90                        'VVVV',
91                        $this->object->getSpIdMax(), // maximum shape identifier increased by one
92                        $this->object->getCDgSaved() + 1, // number of file identifier clusters increased by one
93                        $this->object->getCSpSaved(),
94                        $this->object->getCDgSaved() // count total number of drawings saved
95                    );
96
97                // add file identifier clusters (one per drawing)
98                $IDCLs = $this->object->getIDCLs();
99
100                foreach ($IDCLs as $dgId => $maxReducedSpId) {
101                    $dggData .= pack('VV', $dgId, $maxReducedSpId + 1);
102                }
103
104                $header = pack('vvV', $recVerInstance, $recType, strlen($dggData));
105                $innerData .= $header . $dggData;
106
107                // write the bstoreContainer
108                if ($bstoreContainer = $this->object->getBstoreContainer()) {
109                    $writer = new self($bstoreContainer);
110                    $innerData .= $writer->close();
111                }
112
113                // write the record
114                $recVer = 0xF;
115                $recInstance = 0x0000;
116                $recType = 0xF000;
117                $length = strlen($innerData);
118
119                $recVerInstance = $recVer;
120                $recVerInstance |= $recInstance << 4;
121
122                $header = pack('vvV', $recVerInstance, $recType, $length);
123
124                $this->data = $header . $innerData;
125
126                break;
127            case BstoreContainer::class:
128                // this is a container record
129
130                // initialize
131                $innerData = '';
132
133                // treat the inner data
134                if ($BSECollection = $this->object->getBSECollection()) {
135                    foreach ($BSECollection as $BSE) {
136                        $writer = new self($BSE);
137                        $innerData .= $writer->close();
138                    }
139                }
140
141                // write the record
142                $recVer = 0xF;
143                $recInstance = count($this->object->getBSECollection());
144                $recType = 0xF001;
145                $length = strlen($innerData);
146
147                $recVerInstance = $recVer;
148                $recVerInstance |= $recInstance << 4;
149
150                $header = pack('vvV', $recVerInstance, $recType, $length);
151
152                $this->data = $header . $innerData;
153
154                break;
155            case BSE::class:
156                // this is a semi-container record
157
158                // initialize
159                $innerData = '';
160
161                // here we treat the inner data
162                if ($blip = $this->object->getBlip()) {
163                    $writer = new self($blip);
164                    $innerData .= $writer->close();
165                }
166
167                // initialize
168                $data = '';
169
170                $btWin32 = $this->object->getBlipType();
171                $btMacOS = $this->object->getBlipType();
172                $data .= pack('CC', $btWin32, $btMacOS);
173
174                $rgbUid = pack('VVVV', 0, 0, 0, 0); // todo
175                $data .= $rgbUid;
176
177                $tag = 0;
178                $size = strlen($innerData);
179                $cRef = 1;
180                $foDelay = 0; //todo
181                $unused1 = 0x0;
182                $cbName = 0x0;
183                $unused2 = 0x0;
184                $unused3 = 0x0;
185                $data .= pack('vVVVCCCC', $tag, $size, $cRef, $foDelay, $unused1, $cbName, $unused2, $unused3);
186
187                $data .= $innerData;
188
189                // write the record
190                $recVer = 0x2;
191                $recInstance = $this->object->getBlipType();
192                $recType = 0xF007;
193                $length = strlen($data);
194
195                $recVerInstance = $recVer;
196                $recVerInstance |= $recInstance << 4;
197
198                $header = pack('vvV', $recVerInstance, $recType, $length);
199
200                $this->data = $header;
201
202                $this->data .= $data;
203
204                break;
205            case Blip::class:
206                // this is an atom record
207
208                // write the record
209                switch ($this->object->getParent()->getBlipType()) {
210                    case BSE::BLIPTYPE_JPEG:
211                        // initialize
212                        $innerData = '';
213
214                        $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo
215                        $innerData .= $rgbUid1;
216
217                        $tag = 0xFF; // todo
218                        $innerData .= pack('C', $tag);
219
220                        $innerData .= $this->object->getData();
221
222                        $recVer = 0x0;
223                        $recInstance = 0x46A;
224                        $recType = 0xF01D;
225                        $length = strlen($innerData);
226
227                        $recVerInstance = $recVer;
228                        $recVerInstance |= $recInstance << 4;
229
230                        $header = pack('vvV', $recVerInstance, $recType, $length);
231
232                        $this->data = $header;
233
234                        $this->data .= $innerData;
235
236                        break;
237                    case BSE::BLIPTYPE_PNG:
238                        // initialize
239                        $innerData = '';
240
241                        $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo
242                        $innerData .= $rgbUid1;
243
244                        $tag = 0xFF; // todo
245                        $innerData .= pack('C', $tag);
246
247                        $innerData .= $this->object->getData();
248
249                        $recVer = 0x0;
250                        $recInstance = 0x6E0;
251                        $recType = 0xF01E;
252                        $length = strlen($innerData);
253
254                        $recVerInstance = $recVer;
255                        $recVerInstance |= $recInstance << 4;
256
257                        $header = pack('vvV', $recVerInstance, $recType, $length);
258
259                        $this->data = $header;
260
261                        $this->data .= $innerData;
262
263                        break;
264                }
265
266                break;
267            case DgContainer::class:
268                // this is a container record
269
270                // initialize
271                $innerData = '';
272
273                // write the dg
274                $recVer = 0x0;
275                $recInstance = $this->object->getDgId();
276                $recType = 0xF008;
277                $length = 8;
278
279                $recVerInstance = $recVer;
280                $recVerInstance |= $recInstance << 4;
281
282                $header = pack('vvV', $recVerInstance, $recType, $length);
283
284                // number of shapes in this drawing (including group shape)
285                $countShapes = count($this->object->getSpgrContainer()->getChildren());
286                $innerData .= $header . pack('VV', $countShapes, $this->object->getLastSpId());
287
288                // write the spgrContainer
289                if ($spgrContainer = $this->object->getSpgrContainer()) {
290                    $writer = new self($spgrContainer);
291                    $innerData .= $writer->close();
292
293                    // get the shape offsets relative to the spgrContainer record
294                    $spOffsets = $writer->getSpOffsets();
295                    $spTypes = $writer->getSpTypes();
296
297                    // save the shape offsets relative to dgContainer
298                    foreach ($spOffsets as &$spOffset) {
299                        $spOffset += 24; // add length of dgContainer header data (8 bytes) plus dg data (16 bytes)
300                    }
301
302                    $this->spOffsets = $spOffsets;
303                    $this->spTypes = $spTypes;
304                }
305
306                // write the record
307                $recVer = 0xF;
308                $recInstance = 0x0000;
309                $recType = 0xF002;
310                $length = strlen($innerData);
311
312                $recVerInstance = $recVer;
313                $recVerInstance |= $recInstance << 4;
314
315                $header = pack('vvV', $recVerInstance, $recType, $length);
316
317                $this->data = $header . $innerData;
318
319                break;
320            case SpgrContainer::class:
321                // this is a container record
322
323                // initialize
324                $innerData = '';
325
326                // initialize spape offsets
327                $totalSize = 8;
328                $spOffsets = [];
329                $spTypes = [];
330
331                // treat the inner data
332                foreach ($this->object->getChildren() as $spContainer) {
333                    $writer = new self($spContainer);
334                    $spData = $writer->close();
335                    $innerData .= $spData;
336
337                    // save the shape offsets (where new shape records begin)
338                    $totalSize += strlen($spData);
339                    $spOffsets[] = $totalSize;
340
341                    $spTypes = array_merge($spTypes, $writer->getSpTypes());
342                }
343
344                // write the record
345                $recVer = 0xF;
346                $recInstance = 0x0000;
347                $recType = 0xF003;
348                $length = strlen($innerData);
349
350                $recVerInstance = $recVer;
351                $recVerInstance |= $recInstance << 4;
352
353                $header = pack('vvV', $recVerInstance, $recType, $length);
354
355                $this->data = $header . $innerData;
356                $this->spOffsets = $spOffsets;
357                $this->spTypes = $spTypes;
358
359                break;
360            case SpContainer::class:
361                // initialize
362                $data = '';
363
364                // build the data
365
366                // write group shape record, if necessary?
367                if ($this->object->getSpgr()) {
368                    $recVer = 0x1;
369                    $recInstance = 0x0000;
370                    $recType = 0xF009;
371                    $length = 0x00000010;
372
373                    $recVerInstance = $recVer;
374                    $recVerInstance |= $recInstance << 4;
375
376                    $header = pack('vvV', $recVerInstance, $recType, $length);
377
378                    $data .= $header . pack('VVVV', 0, 0, 0, 0);
379                }
380                $this->spTypes[] = ($this->object->getSpType());
381
382                // write the shape record
383                $recVer = 0x2;
384                $recInstance = $this->object->getSpType(); // shape type
385                $recType = 0xF00A;
386                $length = 0x00000008;
387
388                $recVerInstance = $recVer;
389                $recVerInstance |= $recInstance << 4;
390
391                $header = pack('vvV', $recVerInstance, $recType, $length);
392
393                $data .= $header . pack('VV', $this->object->getSpId(), $this->object->getSpgr() ? 0x0005 : 0x0A00);
394
395                // the options
396                if ($this->object->getOPTCollection()) {
397                    $optData = '';
398
399                    $recVer = 0x3;
400                    $recInstance = count($this->object->getOPTCollection());
401                    $recType = 0xF00B;
402                    foreach ($this->object->getOPTCollection() as $property => $value) {
403                        $optData .= pack('vV', $property, $value);
404                    }
405                    $length = strlen($optData);
406
407                    $recVerInstance = $recVer;
408                    $recVerInstance |= $recInstance << 4;
409
410                    $header = pack('vvV', $recVerInstance, $recType, $length);
411                    $data .= $header . $optData;
412                }
413
414                // the client anchor
415                if ($this->object->getStartCoordinates()) {
416                    $clientAnchorData = '';
417
418                    $recVer = 0x0;
419                    $recInstance = 0x0;
420                    $recType = 0xF010;
421
422                    // start coordinates
423                    [$column, $row] = Coordinate::indexesFromString($this->object->getStartCoordinates());
424                    $c1 = $column - 1;
425                    $r1 = $row - 1;
426
427                    // start offsetX
428                    $startOffsetX = $this->object->getStartOffsetX();
429
430                    // start offsetY
431                    $startOffsetY = $this->object->getStartOffsetY();
432
433                    // end coordinates
434                    [$column, $row] = Coordinate::indexesFromString($this->object->getEndCoordinates());
435                    $c2 = $column - 1;
436                    $r2 = $row - 1;
437
438                    // end offsetX
439                    $endOffsetX = $this->object->getEndOffsetX();
440
441                    // end offsetY
442                    $endOffsetY = $this->object->getEndOffsetY();
443
444                    $clientAnchorData = pack('vvvvvvvvv', $this->object->getSpFlag(), $c1, $startOffsetX, $r1, $startOffsetY, $c2, $endOffsetX, $r2, $endOffsetY);
445
446                    $length = strlen($clientAnchorData);
447
448                    $recVerInstance = $recVer;
449                    $recVerInstance |= $recInstance << 4;
450
451                    $header = pack('vvV', $recVerInstance, $recType, $length);
452                    $data .= $header . $clientAnchorData;
453                }
454
455                // the client data, just empty for now
456                if (!$this->object->getSpgr()) {
457                    $clientDataData = '';
458
459                    $recVer = 0x0;
460                    $recInstance = 0x0;
461                    $recType = 0xF011;
462
463                    $length = strlen($clientDataData);
464
465                    $recVerInstance = $recVer;
466                    $recVerInstance |= $recInstance << 4;
467
468                    $header = pack('vvV', $recVerInstance, $recType, $length);
469                    $data .= $header . $clientDataData;
470                }
471
472                // write the record
473                $recVer = 0xF;
474                $recInstance = 0x0000;
475                $recType = 0xF004;
476                $length = strlen($data);
477
478                $recVerInstance = $recVer;
479                $recVerInstance |= $recInstance << 4;
480
481                $header = pack('vvV', $recVerInstance, $recType, $length);
482
483                $this->data = $header . $data;
484
485                break;
486        }
487
488        return $this->data;
489    }
490
491    /**
492     * Gets the shape offsets.
493     *
494     * @return array
495     */
496    public function getSpOffsets()
497    {
498        return $this->spOffsets;
499    }
500
501    /**
502     * Gets the shape types.
503     *
504     * @return array
505     */
506    public function getSpTypes()
507    {
508        return $this->spTypes;
509    }
510}
511