1<?php
2
3namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
4
5use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
7use PhpOffice\PhpSpreadsheet\Spreadsheet;
8use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
9use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
10use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
11
12class Drawing extends WriterPart
13{
14    /**
15     * Write drawings to XML format.
16     *
17     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet
18     * @param bool $includeCharts Flag indicating if we should include drawing details for charts
19     *
20     * @throws WriterException
21     *
22     * @return string XML Output
23     */
24    public function writeDrawings(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet, $includeCharts = false)
25    {
26        // Create XML writer
27        $objWriter = null;
28        if ($this->getParentWriter()->getUseDiskCaching()) {
29            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
30        } else {
31            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
32        }
33
34        // XML header
35        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
36
37        // xdr:wsDr
38        $objWriter->startElement('xdr:wsDr');
39        $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
40        $objWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
41
42        // Loop through images and write drawings
43        $i = 1;
44        $iterator = $pWorksheet->getDrawingCollection()->getIterator();
45        while ($iterator->valid()) {
46            /** @var BaseDrawing $pDrawing */
47            $pDrawing = $iterator->current();
48            $pRelationId = $i;
49            $hlinkClickId = $pDrawing->getHyperlink() === null ? null : ++$i;
50
51            $this->writeDrawing($objWriter, $pDrawing, $pRelationId, $hlinkClickId);
52
53            $iterator->next();
54            ++$i;
55        }
56
57        if ($includeCharts) {
58            $chartCount = $pWorksheet->getChartCount();
59            // Loop through charts and write the chart position
60            if ($chartCount > 0) {
61                for ($c = 0; $c < $chartCount; ++$c) {
62                    $this->writeChart($objWriter, $pWorksheet->getChartByIndex($c), $c + $i);
63                }
64            }
65        }
66
67        // unparsed AlternateContent
68        $unparsedLoadedData = $pWorksheet->getParent()->getUnparsedLoadedData();
69        if (isset($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingAlternateContents'])) {
70            foreach ($unparsedLoadedData['sheets'][$pWorksheet->getCodeName()]['drawingAlternateContents'] as $drawingAlternateContent) {
71                $objWriter->writeRaw($drawingAlternateContent);
72            }
73        }
74
75        $objWriter->endElement();
76
77        // Return
78        return $objWriter->getData();
79    }
80
81    /**
82     * Write drawings to XML format.
83     *
84     * @param XMLWriter $objWriter XML Writer
85     * @param \PhpOffice\PhpSpreadsheet\Chart\Chart $pChart
86     * @param int $pRelationId
87     */
88    public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart\Chart $pChart, $pRelationId = -1)
89    {
90        $tl = $pChart->getTopLeftPosition();
91        $tl['colRow'] = Coordinate::coordinateFromString($tl['cell']);
92        $br = $pChart->getBottomRightPosition();
93        $br['colRow'] = Coordinate::coordinateFromString($br['cell']);
94
95        $objWriter->startElement('xdr:twoCellAnchor');
96
97        $objWriter->startElement('xdr:from');
98        $objWriter->writeElement('xdr:col', Coordinate::columnIndexFromString($tl['colRow'][0]) - 1);
99        $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset']));
100        $objWriter->writeElement('xdr:row', $tl['colRow'][1] - 1);
101        $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset']));
102        $objWriter->endElement();
103        $objWriter->startElement('xdr:to');
104        $objWriter->writeElement('xdr:col', Coordinate::columnIndexFromString($br['colRow'][0]) - 1);
105        $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset']));
106        $objWriter->writeElement('xdr:row', $br['colRow'][1] - 1);
107        $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset']));
108        $objWriter->endElement();
109
110        $objWriter->startElement('xdr:graphicFrame');
111        $objWriter->writeAttribute('macro', '');
112        $objWriter->startElement('xdr:nvGraphicFramePr');
113        $objWriter->startElement('xdr:cNvPr');
114        $objWriter->writeAttribute('name', 'Chart ' . $pRelationId);
115        $objWriter->writeAttribute('id', 1025 * $pRelationId);
116        $objWriter->endElement();
117        $objWriter->startElement('xdr:cNvGraphicFramePr');
118        $objWriter->startElement('a:graphicFrameLocks');
119        $objWriter->endElement();
120        $objWriter->endElement();
121        $objWriter->endElement();
122
123        $objWriter->startElement('xdr:xfrm');
124        $objWriter->startElement('a:off');
125        $objWriter->writeAttribute('x', '0');
126        $objWriter->writeAttribute('y', '0');
127        $objWriter->endElement();
128        $objWriter->startElement('a:ext');
129        $objWriter->writeAttribute('cx', '0');
130        $objWriter->writeAttribute('cy', '0');
131        $objWriter->endElement();
132        $objWriter->endElement();
133
134        $objWriter->startElement('a:graphic');
135        $objWriter->startElement('a:graphicData');
136        $objWriter->writeAttribute('uri', 'http://schemas.openxmlformats.org/drawingml/2006/chart');
137        $objWriter->startElement('c:chart');
138        $objWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart');
139        $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
140        $objWriter->writeAttribute('r:id', 'rId' . $pRelationId);
141        $objWriter->endElement();
142        $objWriter->endElement();
143        $objWriter->endElement();
144        $objWriter->endElement();
145
146        $objWriter->startElement('xdr:clientData');
147        $objWriter->endElement();
148
149        $objWriter->endElement();
150    }
151
152    /**
153     * Write drawings to XML format.
154     *
155     * @param XMLWriter $objWriter XML Writer
156     * @param BaseDrawing $pDrawing
157     * @param int $pRelationId
158     * @param null|int $hlinkClickId
159     *
160     * @throws WriterException
161     */
162    public function writeDrawing(XMLWriter $objWriter, BaseDrawing $pDrawing, $pRelationId = -1, $hlinkClickId = null)
163    {
164        if ($pRelationId >= 0) {
165            // xdr:oneCellAnchor
166            $objWriter->startElement('xdr:oneCellAnchor');
167            // Image location
168            $aCoordinates = Coordinate::coordinateFromString($pDrawing->getCoordinates());
169            $aCoordinates[0] = Coordinate::columnIndexFromString($aCoordinates[0]);
170
171            // xdr:from
172            $objWriter->startElement('xdr:from');
173            $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1);
174            $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getOffsetX()));
175            $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1);
176            $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getOffsetY()));
177            $objWriter->endElement();
178
179            // xdr:ext
180            $objWriter->startElement('xdr:ext');
181            $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getWidth()));
182            $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getHeight()));
183            $objWriter->endElement();
184
185            // xdr:pic
186            $objWriter->startElement('xdr:pic');
187
188            // xdr:nvPicPr
189            $objWriter->startElement('xdr:nvPicPr');
190
191            // xdr:cNvPr
192            $objWriter->startElement('xdr:cNvPr');
193            $objWriter->writeAttribute('id', $pRelationId);
194            $objWriter->writeAttribute('name', $pDrawing->getName());
195            $objWriter->writeAttribute('descr', $pDrawing->getDescription());
196
197            //a:hlinkClick
198            $this->writeHyperLinkDrawing($objWriter, $hlinkClickId);
199
200            $objWriter->endElement();
201
202            // xdr:cNvPicPr
203            $objWriter->startElement('xdr:cNvPicPr');
204
205            // a:picLocks
206            $objWriter->startElement('a:picLocks');
207            $objWriter->writeAttribute('noChangeAspect', '1');
208            $objWriter->endElement();
209
210            $objWriter->endElement();
211
212            $objWriter->endElement();
213
214            // xdr:blipFill
215            $objWriter->startElement('xdr:blipFill');
216
217            // a:blip
218            $objWriter->startElement('a:blip');
219            $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
220            $objWriter->writeAttribute('r:embed', 'rId' . $pRelationId);
221            $objWriter->endElement();
222
223            // a:stretch
224            $objWriter->startElement('a:stretch');
225            $objWriter->writeElement('a:fillRect', null);
226            $objWriter->endElement();
227
228            $objWriter->endElement();
229
230            // xdr:spPr
231            $objWriter->startElement('xdr:spPr');
232
233            // a:xfrm
234            $objWriter->startElement('a:xfrm');
235            $objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($pDrawing->getRotation()));
236            $objWriter->endElement();
237
238            // a:prstGeom
239            $objWriter->startElement('a:prstGeom');
240            $objWriter->writeAttribute('prst', 'rect');
241
242            // a:avLst
243            $objWriter->writeElement('a:avLst', null);
244
245            $objWriter->endElement();
246
247            if ($pDrawing->getShadow()->getVisible()) {
248                // a:effectLst
249                $objWriter->startElement('a:effectLst');
250
251                // a:outerShdw
252                $objWriter->startElement('a:outerShdw');
253                $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getShadow()->getBlurRadius()));
254                $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($pDrawing->getShadow()->getDistance()));
255                $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($pDrawing->getShadow()->getDirection()));
256                $objWriter->writeAttribute('algn', $pDrawing->getShadow()->getAlignment());
257                $objWriter->writeAttribute('rotWithShape', '0');
258
259                // a:srgbClr
260                $objWriter->startElement('a:srgbClr');
261                $objWriter->writeAttribute('val', $pDrawing->getShadow()->getColor()->getRGB());
262
263                // a:alpha
264                $objWriter->startElement('a:alpha');
265                $objWriter->writeAttribute('val', $pDrawing->getShadow()->getAlpha() * 1000);
266                $objWriter->endElement();
267
268                $objWriter->endElement();
269
270                $objWriter->endElement();
271
272                $objWriter->endElement();
273            }
274            $objWriter->endElement();
275
276            $objWriter->endElement();
277
278            // xdr:clientData
279            $objWriter->writeElement('xdr:clientData', null);
280
281            $objWriter->endElement();
282        } else {
283            throw new WriterException('Invalid parameters passed.');
284        }
285    }
286
287    /**
288     * Write VML header/footer images to XML format.
289     *
290     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet
291     *
292     * @throws WriterException
293     *
294     * @return string XML Output
295     */
296    public function writeVMLHeaderFooterImages(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWorksheet)
297    {
298        // Create XML writer
299        $objWriter = null;
300        if ($this->getParentWriter()->getUseDiskCaching()) {
301            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
302        } else {
303            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
304        }
305
306        // XML header
307        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
308
309        // Header/footer images
310        $images = $pWorksheet->getHeaderFooter()->getImages();
311
312        // xml
313        $objWriter->startElement('xml');
314        $objWriter->writeAttribute('xmlns:v', 'urn:schemas-microsoft-com:vml');
315        $objWriter->writeAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office');
316        $objWriter->writeAttribute('xmlns:x', 'urn:schemas-microsoft-com:office:excel');
317
318        // o:shapelayout
319        $objWriter->startElement('o:shapelayout');
320        $objWriter->writeAttribute('v:ext', 'edit');
321
322        // o:idmap
323        $objWriter->startElement('o:idmap');
324        $objWriter->writeAttribute('v:ext', 'edit');
325        $objWriter->writeAttribute('data', '1');
326        $objWriter->endElement();
327
328        $objWriter->endElement();
329
330        // v:shapetype
331        $objWriter->startElement('v:shapetype');
332        $objWriter->writeAttribute('id', '_x0000_t75');
333        $objWriter->writeAttribute('coordsize', '21600,21600');
334        $objWriter->writeAttribute('o:spt', '75');
335        $objWriter->writeAttribute('o:preferrelative', 't');
336        $objWriter->writeAttribute('path', 'm@4@5l@4@11@9@11@9@5xe');
337        $objWriter->writeAttribute('filled', 'f');
338        $objWriter->writeAttribute('stroked', 'f');
339
340        // v:stroke
341        $objWriter->startElement('v:stroke');
342        $objWriter->writeAttribute('joinstyle', 'miter');
343        $objWriter->endElement();
344
345        // v:formulas
346        $objWriter->startElement('v:formulas');
347
348        // v:f
349        $objWriter->startElement('v:f');
350        $objWriter->writeAttribute('eqn', 'if lineDrawn pixelLineWidth 0');
351        $objWriter->endElement();
352
353        // v:f
354        $objWriter->startElement('v:f');
355        $objWriter->writeAttribute('eqn', 'sum @0 1 0');
356        $objWriter->endElement();
357
358        // v:f
359        $objWriter->startElement('v:f');
360        $objWriter->writeAttribute('eqn', 'sum 0 0 @1');
361        $objWriter->endElement();
362
363        // v:f
364        $objWriter->startElement('v:f');
365        $objWriter->writeAttribute('eqn', 'prod @2 1 2');
366        $objWriter->endElement();
367
368        // v:f
369        $objWriter->startElement('v:f');
370        $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelWidth');
371        $objWriter->endElement();
372
373        // v:f
374        $objWriter->startElement('v:f');
375        $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelHeight');
376        $objWriter->endElement();
377
378        // v:f
379        $objWriter->startElement('v:f');
380        $objWriter->writeAttribute('eqn', 'sum @0 0 1');
381        $objWriter->endElement();
382
383        // v:f
384        $objWriter->startElement('v:f');
385        $objWriter->writeAttribute('eqn', 'prod @6 1 2');
386        $objWriter->endElement();
387
388        // v:f
389        $objWriter->startElement('v:f');
390        $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelWidth');
391        $objWriter->endElement();
392
393        // v:f
394        $objWriter->startElement('v:f');
395        $objWriter->writeAttribute('eqn', 'sum @8 21600 0');
396        $objWriter->endElement();
397
398        // v:f
399        $objWriter->startElement('v:f');
400        $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelHeight');
401        $objWriter->endElement();
402
403        // v:f
404        $objWriter->startElement('v:f');
405        $objWriter->writeAttribute('eqn', 'sum @10 21600 0');
406        $objWriter->endElement();
407
408        $objWriter->endElement();
409
410        // v:path
411        $objWriter->startElement('v:path');
412        $objWriter->writeAttribute('o:extrusionok', 'f');
413        $objWriter->writeAttribute('gradientshapeok', 't');
414        $objWriter->writeAttribute('o:connecttype', 'rect');
415        $objWriter->endElement();
416
417        // o:lock
418        $objWriter->startElement('o:lock');
419        $objWriter->writeAttribute('v:ext', 'edit');
420        $objWriter->writeAttribute('aspectratio', 't');
421        $objWriter->endElement();
422
423        $objWriter->endElement();
424
425        // Loop through images
426        foreach ($images as $key => $value) {
427            $this->writeVMLHeaderFooterImage($objWriter, $key, $value);
428        }
429
430        $objWriter->endElement();
431
432        // Return
433        return $objWriter->getData();
434    }
435
436    /**
437     * Write VML comment to XML format.
438     *
439     * @param XMLWriter $objWriter XML Writer
440     * @param string $pReference Reference
441     * @param HeaderFooterDrawing $pImage Image
442     */
443    private function writeVMLHeaderFooterImage(XMLWriter $objWriter, $pReference, HeaderFooterDrawing $pImage)
444    {
445        // Calculate object id
446        preg_match('{(\d+)}', md5($pReference), $m);
447        $id = 1500 + (substr($m[1], 0, 2) * 1);
448
449        // Calculate offset
450        $width = $pImage->getWidth();
451        $height = $pImage->getHeight();
452        $marginLeft = $pImage->getOffsetX();
453        $marginTop = $pImage->getOffsetY();
454
455        // v:shape
456        $objWriter->startElement('v:shape');
457        $objWriter->writeAttribute('id', $pReference);
458        $objWriter->writeAttribute('o:spid', '_x0000_s' . $id);
459        $objWriter->writeAttribute('type', '#_x0000_t75');
460        $objWriter->writeAttribute('style', "position:absolute;margin-left:{$marginLeft}px;margin-top:{$marginTop}px;width:{$width}px;height:{$height}px;z-index:1");
461
462        // v:imagedata
463        $objWriter->startElement('v:imagedata');
464        $objWriter->writeAttribute('o:relid', 'rId' . $pReference);
465        $objWriter->writeAttribute('o:title', $pImage->getName());
466        $objWriter->endElement();
467
468        // o:lock
469        $objWriter->startElement('o:lock');
470        $objWriter->writeAttribute('v:ext', 'edit');
471        $objWriter->writeAttribute('textRotation', 't');
472        $objWriter->endElement();
473
474        $objWriter->endElement();
475    }
476
477    /**
478     * Get an array of all drawings.
479     *
480     * @param Spreadsheet $spreadsheet
481     *
482     * @return \PhpOffice\PhpSpreadsheet\Worksheet\Drawing[] All drawings in PhpSpreadsheet
483     */
484    public function allDrawings(Spreadsheet $spreadsheet)
485    {
486        // Get an array of all drawings
487        $aDrawings = [];
488
489        // Loop through PhpSpreadsheet
490        $sheetCount = $spreadsheet->getSheetCount();
491        for ($i = 0; $i < $sheetCount; ++$i) {
492            // Loop through images and add to array
493            $iterator = $spreadsheet->getSheet($i)->getDrawingCollection()->getIterator();
494            while ($iterator->valid()) {
495                $aDrawings[] = $iterator->current();
496
497                $iterator->next();
498            }
499        }
500
501        return $aDrawings;
502    }
503
504    /**
505     * @param XMLWriter $objWriter
506     * @param null|int $hlinkClickId
507     */
508    private function writeHyperLinkDrawing(XMLWriter $objWriter, $hlinkClickId)
509    {
510        if ($hlinkClickId === null) {
511            return;
512        }
513
514        $objWriter->startElement('a:hlinkClick');
515        $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
516        $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId);
517        $objWriter->endElement();
518    }
519}
520