1<?php
2
3namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
4
5use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6use PhpOffice\PhpSpreadsheet\Reader\Xls;
7use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
8use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
9use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
10use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
11use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
12use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
13use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
14
15class Escher
16{
17    const DGGCONTAINER = 0xF000;
18    const BSTORECONTAINER = 0xF001;
19    const DGCONTAINER = 0xF002;
20    const SPGRCONTAINER = 0xF003;
21    const SPCONTAINER = 0xF004;
22    const DGG = 0xF006;
23    const BSE = 0xF007;
24    const DG = 0xF008;
25    const SPGR = 0xF009;
26    const SP = 0xF00A;
27    const OPT = 0xF00B;
28    const CLIENTTEXTBOX = 0xF00D;
29    const CLIENTANCHOR = 0xF010;
30    const CLIENTDATA = 0xF011;
31    const BLIPJPEG = 0xF01D;
32    const BLIPPNG = 0xF01E;
33    const SPLITMENUCOLORS = 0xF11E;
34    const TERTIARYOPT = 0xF122;
35
36    /**
37     * Escher stream data (binary).
38     *
39     * @var string
40     */
41    private $data;
42
43    /**
44     * Size in bytes of the Escher stream data.
45     *
46     * @var int
47     */
48    private $dataSize;
49
50    /**
51     * Current position of stream pointer in Escher stream data.
52     *
53     * @var int
54     */
55    private $pos;
56
57    /**
58     * The object to be returned by the reader. Modified during load.
59     *
60     * @var BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer
61     */
62    private $object;
63
64    /**
65     * Create a new Escher instance.
66     *
67     * @param mixed $object
68     */
69    public function __construct($object)
70    {
71        $this->object = $object;
72    }
73
74    /**
75     * Load Escher stream data. May be a partial Escher stream.
76     *
77     * @param string $data
78     *
79     * @return BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer
80     */
81    public function load($data)
82    {
83        $this->data = $data;
84
85        // total byte size of Excel data (workbook global substream + sheet substreams)
86        $this->dataSize = strlen($this->data);
87
88        $this->pos = 0;
89
90        // Parse Escher stream
91        while ($this->pos < $this->dataSize) {
92            // offset: 2; size: 2: Record Type
93            $fbt = Xls::getUInt2d($this->data, $this->pos + 2);
94
95            switch ($fbt) {
96                case self::DGGCONTAINER:
97                    $this->readDggContainer();
98
99                    break;
100                case self::DGG:
101                    $this->readDgg();
102
103                    break;
104                case self::BSTORECONTAINER:
105                    $this->readBstoreContainer();
106
107                    break;
108                case self::BSE:
109                    $this->readBSE();
110
111                    break;
112                case self::BLIPJPEG:
113                    $this->readBlipJPEG();
114
115                    break;
116                case self::BLIPPNG:
117                    $this->readBlipPNG();
118
119                    break;
120                case self::OPT:
121                    $this->readOPT();
122
123                    break;
124                case self::TERTIARYOPT:
125                    $this->readTertiaryOPT();
126
127                    break;
128                case self::SPLITMENUCOLORS:
129                    $this->readSplitMenuColors();
130
131                    break;
132                case self::DGCONTAINER:
133                    $this->readDgContainer();
134
135                    break;
136                case self::DG:
137                    $this->readDg();
138
139                    break;
140                case self::SPGRCONTAINER:
141                    $this->readSpgrContainer();
142
143                    break;
144                case self::SPCONTAINER:
145                    $this->readSpContainer();
146
147                    break;
148                case self::SPGR:
149                    $this->readSpgr();
150
151                    break;
152                case self::SP:
153                    $this->readSp();
154
155                    break;
156                case self::CLIENTTEXTBOX:
157                    $this->readClientTextbox();
158
159                    break;
160                case self::CLIENTANCHOR:
161                    $this->readClientAnchor();
162
163                    break;
164                case self::CLIENTDATA:
165                    $this->readClientData();
166
167                    break;
168                default:
169                    $this->readDefault();
170
171                    break;
172            }
173        }
174
175        return $this->object;
176    }
177
178    /**
179     * Read a generic record.
180     */
181    private function readDefault()
182    {
183        // offset 0; size: 2; recVer and recInstance
184        $verInstance = Xls::getUInt2d($this->data, $this->pos);
185
186        // offset: 2; size: 2: Record Type
187        $fbt = Xls::getUInt2d($this->data, $this->pos + 2);
188
189        // bit: 0-3; mask: 0x000F; recVer
190        $recVer = (0x000F & $verInstance) >> 0;
191
192        $length = Xls::getInt4d($this->data, $this->pos + 4);
193        $recordData = substr($this->data, $this->pos + 8, $length);
194
195        // move stream pointer to next record
196        $this->pos += 8 + $length;
197    }
198
199    /**
200     * Read DggContainer record (Drawing Group Container).
201     */
202    private function readDggContainer()
203    {
204        $length = Xls::getInt4d($this->data, $this->pos + 4);
205        $recordData = substr($this->data, $this->pos + 8, $length);
206
207        // move stream pointer to next record
208        $this->pos += 8 + $length;
209
210        // record is a container, read contents
211        $dggContainer = new DggContainer();
212        $this->object->setDggContainer($dggContainer);
213        $reader = new self($dggContainer);
214        $reader->load($recordData);
215    }
216
217    /**
218     * Read Dgg record (Drawing Group).
219     */
220    private function readDgg()
221    {
222        $length = Xls::getInt4d($this->data, $this->pos + 4);
223        $recordData = substr($this->data, $this->pos + 8, $length);
224
225        // move stream pointer to next record
226        $this->pos += 8 + $length;
227    }
228
229    /**
230     * Read BstoreContainer record (Blip Store Container).
231     */
232    private function readBstoreContainer()
233    {
234        $length = Xls::getInt4d($this->data, $this->pos + 4);
235        $recordData = substr($this->data, $this->pos + 8, $length);
236
237        // move stream pointer to next record
238        $this->pos += 8 + $length;
239
240        // record is a container, read contents
241        $bstoreContainer = new BstoreContainer();
242        $this->object->setBstoreContainer($bstoreContainer);
243        $reader = new self($bstoreContainer);
244        $reader->load($recordData);
245    }
246
247    /**
248     * Read BSE record.
249     */
250    private function readBSE()
251    {
252        // offset: 0; size: 2; recVer and recInstance
253
254        // bit: 4-15; mask: 0xFFF0; recInstance
255        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
256
257        $length = Xls::getInt4d($this->data, $this->pos + 4);
258        $recordData = substr($this->data, $this->pos + 8, $length);
259
260        // move stream pointer to next record
261        $this->pos += 8 + $length;
262
263        // add BSE to BstoreContainer
264        $BSE = new BSE();
265        $this->object->addBSE($BSE);
266
267        $BSE->setBLIPType($recInstance);
268
269        // offset: 0; size: 1; btWin32 (MSOBLIPTYPE)
270        $btWin32 = ord($recordData[0]);
271
272        // offset: 1; size: 1; btWin32 (MSOBLIPTYPE)
273        $btMacOS = ord($recordData[1]);
274
275        // offset: 2; size: 16; MD4 digest
276        $rgbUid = substr($recordData, 2, 16);
277
278        // offset: 18; size: 2; tag
279        $tag = Xls::getUInt2d($recordData, 18);
280
281        // offset: 20; size: 4; size of BLIP in bytes
282        $size = Xls::getInt4d($recordData, 20);
283
284        // offset: 24; size: 4; number of references to this BLIP
285        $cRef = Xls::getInt4d($recordData, 24);
286
287        // offset: 28; size: 4; MSOFO file offset
288        $foDelay = Xls::getInt4d($recordData, 28);
289
290        // offset: 32; size: 1; unused1
291        $unused1 = ord($recordData[32]);
292
293        // offset: 33; size: 1; size of nameData in bytes (including null terminator)
294        $cbName = ord($recordData[33]);
295
296        // offset: 34; size: 1; unused2
297        $unused2 = ord($recordData[34]);
298
299        // offset: 35; size: 1; unused3
300        $unused3 = ord($recordData[35]);
301
302        // offset: 36; size: $cbName; nameData
303        $nameData = substr($recordData, 36, $cbName);
304
305        // offset: 36 + $cbName, size: var; the BLIP data
306        $blipData = substr($recordData, 36 + $cbName);
307
308        // record is a container, read contents
309        $reader = new self($BSE);
310        $reader->load($blipData);
311    }
312
313    /**
314     * Read BlipJPEG record. Holds raw JPEG image data.
315     */
316    private function readBlipJPEG()
317    {
318        // offset: 0; size: 2; recVer and recInstance
319
320        // bit: 4-15; mask: 0xFFF0; recInstance
321        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
322
323        $length = Xls::getInt4d($this->data, $this->pos + 4);
324        $recordData = substr($this->data, $this->pos + 8, $length);
325
326        // move stream pointer to next record
327        $this->pos += 8 + $length;
328
329        $pos = 0;
330
331        // offset: 0; size: 16; rgbUid1 (MD4 digest of)
332        $rgbUid1 = substr($recordData, 0, 16);
333        $pos += 16;
334
335        // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3
336        if (in_array($recInstance, [0x046B, 0x06E3])) {
337            $rgbUid2 = substr($recordData, 16, 16);
338            $pos += 16;
339        }
340
341        // offset: var; size: 1; tag
342        $tag = ord($recordData[$pos]);
343        $pos += 1;
344
345        // offset: var; size: var; the raw image data
346        $data = substr($recordData, $pos);
347
348        $blip = new Blip();
349        $blip->setData($data);
350
351        $this->object->setBlip($blip);
352    }
353
354    /**
355     * Read BlipPNG record. Holds raw PNG image data.
356     */
357    private function readBlipPNG()
358    {
359        // offset: 0; size: 2; recVer and recInstance
360
361        // bit: 4-15; mask: 0xFFF0; recInstance
362        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
363
364        $length = Xls::getInt4d($this->data, $this->pos + 4);
365        $recordData = substr($this->data, $this->pos + 8, $length);
366
367        // move stream pointer to next record
368        $this->pos += 8 + $length;
369
370        $pos = 0;
371
372        // offset: 0; size: 16; rgbUid1 (MD4 digest of)
373        $rgbUid1 = substr($recordData, 0, 16);
374        $pos += 16;
375
376        // offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3
377        if ($recInstance == 0x06E1) {
378            $rgbUid2 = substr($recordData, 16, 16);
379            $pos += 16;
380        }
381
382        // offset: var; size: 1; tag
383        $tag = ord($recordData[$pos]);
384        $pos += 1;
385
386        // offset: var; size: var; the raw image data
387        $data = substr($recordData, $pos);
388
389        $blip = new Blip();
390        $blip->setData($data);
391
392        $this->object->setBlip($blip);
393    }
394
395    /**
396     * Read OPT record. This record may occur within DggContainer record or SpContainer.
397     */
398    private function readOPT()
399    {
400        // offset: 0; size: 2; recVer and recInstance
401
402        // bit: 4-15; mask: 0xFFF0; recInstance
403        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
404
405        $length = Xls::getInt4d($this->data, $this->pos + 4);
406        $recordData = substr($this->data, $this->pos + 8, $length);
407
408        // move stream pointer to next record
409        $this->pos += 8 + $length;
410
411        $this->readOfficeArtRGFOPTE($recordData, $recInstance);
412    }
413
414    /**
415     * Read TertiaryOPT record.
416     */
417    private function readTertiaryOPT()
418    {
419        // offset: 0; size: 2; recVer and recInstance
420
421        // bit: 4-15; mask: 0xFFF0; recInstance
422        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
423
424        $length = Xls::getInt4d($this->data, $this->pos + 4);
425        $recordData = substr($this->data, $this->pos + 8, $length);
426
427        // move stream pointer to next record
428        $this->pos += 8 + $length;
429    }
430
431    /**
432     * Read SplitMenuColors record.
433     */
434    private function readSplitMenuColors()
435    {
436        $length = Xls::getInt4d($this->data, $this->pos + 4);
437        $recordData = substr($this->data, $this->pos + 8, $length);
438
439        // move stream pointer to next record
440        $this->pos += 8 + $length;
441    }
442
443    /**
444     * Read DgContainer record (Drawing Container).
445     */
446    private function readDgContainer()
447    {
448        $length = Xls::getInt4d($this->data, $this->pos + 4);
449        $recordData = substr($this->data, $this->pos + 8, $length);
450
451        // move stream pointer to next record
452        $this->pos += 8 + $length;
453
454        // record is a container, read contents
455        $dgContainer = new DgContainer();
456        $this->object->setDgContainer($dgContainer);
457        $reader = new self($dgContainer);
458        $escher = $reader->load($recordData);
459    }
460
461    /**
462     * Read Dg record (Drawing).
463     */
464    private function readDg()
465    {
466        $length = Xls::getInt4d($this->data, $this->pos + 4);
467        $recordData = substr($this->data, $this->pos + 8, $length);
468
469        // move stream pointer to next record
470        $this->pos += 8 + $length;
471    }
472
473    /**
474     * Read SpgrContainer record (Shape Group Container).
475     */
476    private function readSpgrContainer()
477    {
478        // context is either context DgContainer or SpgrContainer
479
480        $length = Xls::getInt4d($this->data, $this->pos + 4);
481        $recordData = substr($this->data, $this->pos + 8, $length);
482
483        // move stream pointer to next record
484        $this->pos += 8 + $length;
485
486        // record is a container, read contents
487        $spgrContainer = new SpgrContainer();
488
489        if ($this->object instanceof DgContainer) {
490            // DgContainer
491            $this->object->setSpgrContainer($spgrContainer);
492        } else {
493            // SpgrContainer
494            $this->object->addChild($spgrContainer);
495        }
496
497        $reader = new self($spgrContainer);
498        $escher = $reader->load($recordData);
499    }
500
501    /**
502     * Read SpContainer record (Shape Container).
503     */
504    private function readSpContainer()
505    {
506        $length = Xls::getInt4d($this->data, $this->pos + 4);
507        $recordData = substr($this->data, $this->pos + 8, $length);
508
509        // add spContainer to spgrContainer
510        $spContainer = new SpContainer();
511        $this->object->addChild($spContainer);
512
513        // move stream pointer to next record
514        $this->pos += 8 + $length;
515
516        // record is a container, read contents
517        $reader = new self($spContainer);
518        $escher = $reader->load($recordData);
519    }
520
521    /**
522     * Read Spgr record (Shape Group).
523     */
524    private function readSpgr()
525    {
526        $length = Xls::getInt4d($this->data, $this->pos + 4);
527        $recordData = substr($this->data, $this->pos + 8, $length);
528
529        // move stream pointer to next record
530        $this->pos += 8 + $length;
531    }
532
533    /**
534     * Read Sp record (Shape).
535     */
536    private function readSp()
537    {
538        // offset: 0; size: 2; recVer and recInstance
539
540        // bit: 4-15; mask: 0xFFF0; recInstance
541        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
542
543        $length = Xls::getInt4d($this->data, $this->pos + 4);
544        $recordData = substr($this->data, $this->pos + 8, $length);
545
546        // move stream pointer to next record
547        $this->pos += 8 + $length;
548    }
549
550    /**
551     * Read ClientTextbox record.
552     */
553    private function readClientTextbox()
554    {
555        // offset: 0; size: 2; recVer and recInstance
556
557        // bit: 4-15; mask: 0xFFF0; recInstance
558        $recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
559
560        $length = Xls::getInt4d($this->data, $this->pos + 4);
561        $recordData = substr($this->data, $this->pos + 8, $length);
562
563        // move stream pointer to next record
564        $this->pos += 8 + $length;
565    }
566
567    /**
568     * Read ClientAnchor record. This record holds information about where the shape is anchored in worksheet.
569     */
570    private function readClientAnchor()
571    {
572        $length = Xls::getInt4d($this->data, $this->pos + 4);
573        $recordData = substr($this->data, $this->pos + 8, $length);
574
575        // move stream pointer to next record
576        $this->pos += 8 + $length;
577
578        // offset: 2; size: 2; upper-left corner column index (0-based)
579        $c1 = Xls::getUInt2d($recordData, 2);
580
581        // offset: 4; size: 2; upper-left corner horizontal offset in 1/1024 of column width
582        $startOffsetX = Xls::getUInt2d($recordData, 4);
583
584        // offset: 6; size: 2; upper-left corner row index (0-based)
585        $r1 = Xls::getUInt2d($recordData, 6);
586
587        // offset: 8; size: 2; upper-left corner vertical offset in 1/256 of row height
588        $startOffsetY = Xls::getUInt2d($recordData, 8);
589
590        // offset: 10; size: 2; bottom-right corner column index (0-based)
591        $c2 = Xls::getUInt2d($recordData, 10);
592
593        // offset: 12; size: 2; bottom-right corner horizontal offset in 1/1024 of column width
594        $endOffsetX = Xls::getUInt2d($recordData, 12);
595
596        // offset: 14; size: 2; bottom-right corner row index (0-based)
597        $r2 = Xls::getUInt2d($recordData, 14);
598
599        // offset: 16; size: 2; bottom-right corner vertical offset in 1/256 of row height
600        $endOffsetY = Xls::getUInt2d($recordData, 16);
601
602        // set the start coordinates
603        $this->object->setStartCoordinates(Coordinate::stringFromColumnIndex($c1 + 1) . ($r1 + 1));
604
605        // set the start offsetX
606        $this->object->setStartOffsetX($startOffsetX);
607
608        // set the start offsetY
609        $this->object->setStartOffsetY($startOffsetY);
610
611        // set the end coordinates
612        $this->object->setEndCoordinates(Coordinate::stringFromColumnIndex($c2 + 1) . ($r2 + 1));
613
614        // set the end offsetX
615        $this->object->setEndOffsetX($endOffsetX);
616
617        // set the end offsetY
618        $this->object->setEndOffsetY($endOffsetY);
619    }
620
621    /**
622     * Read ClientData record.
623     */
624    private function readClientData()
625    {
626        $length = Xls::getInt4d($this->data, $this->pos + 4);
627        $recordData = substr($this->data, $this->pos + 8, $length);
628
629        // move stream pointer to next record
630        $this->pos += 8 + $length;
631    }
632
633    /**
634     * Read OfficeArtRGFOPTE table of property-value pairs.
635     *
636     * @param string $data Binary data
637     * @param int $n Number of properties
638     */
639    private function readOfficeArtRGFOPTE($data, $n)
640    {
641        $splicedComplexData = substr($data, 6 * $n);
642
643        // loop through property-value pairs
644        for ($i = 0; $i < $n; ++$i) {
645            // read 6 bytes at a time
646            $fopte = substr($data, 6 * $i, 6);
647
648            // offset: 0; size: 2; opid
649            $opid = Xls::getUInt2d($fopte, 0);
650
651            // bit: 0-13; mask: 0x3FFF; opid.opid
652            $opidOpid = (0x3FFF & $opid) >> 0;
653
654            // bit: 14; mask 0x4000; 1 = value in op field is BLIP identifier
655            $opidFBid = (0x4000 & $opid) >> 14;
656
657            // bit: 15; mask 0x8000; 1 = this is a complex property, op field specifies size of complex data
658            $opidFComplex = (0x8000 & $opid) >> 15;
659
660            // offset: 2; size: 4; the value for this property
661            $op = Xls::getInt4d($fopte, 2);
662
663            if ($opidFComplex) {
664                $complexData = substr($splicedComplexData, 0, $op);
665                $splicedComplexData = substr($splicedComplexData, $op);
666
667                // we store string value with complex data
668                $value = $complexData;
669            } else {
670                // we store integer value
671                $value = $op;
672            }
673
674            $this->object->setOPT($opidOpid, $value);
675        }
676    }
677}
678