Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.15% covered (warning)
69.15%
269 / 389
55.56% covered (warning)
55.56%
10 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ODPresentation
69.15% covered (warning)
69.15%
269 / 389
55.56% covered (warning)
55.56%
10 / 18
1119.59
0.00% covered (danger)
0.00%
0 / 1
 canRead
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fileSupportsUnserializePhpPresentation
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 load
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 loadFile
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 loadDocumentProperties
51.16% covered (warning)
51.16%
22 / 43
0.00% covered (danger)
0.00%
0 / 1
36.83
 loadSlides
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
7
 loadPresentationProperties
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 loadStyle
56.28% covered (warning)
56.28%
103 / 183
0.00% covered (danger)
0.00%
0 / 1
505.09
 loadSlide
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 loadShapeDrawing
87.50% covered (warning)
87.50%
28 / 32
0.00% covered (danger)
0.00%
0 / 1
15.44
 loadShapeRichText
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 readParagraph
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
11.84
 readParagraphItem
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 readList
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 readListItem
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 loadStylesFile
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
12.41
 getExpressionUnit
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getExpressionValue
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * This file is part of PHPPresentation - A pure PHP library for reading and writing
4 * presentations documents.
5 *
6 * PHPPresentation is free software distributed under the terms of the GNU Lesser
7 * General Public License version 3 as published by the Free Software Foundation.
8 *
9 * For the full copyright and license information, please read the LICENSE
10 * file that was distributed with this source code. For the full list of
11 * contributors, visit https://github.com/PHPOffice/PHPPresentation/contributors.
12 *
13 * @see        https://github.com/PHPOffice/PHPPresentation
14 *
15 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16 */
17
18declare(strict_types=1);
19
20namespace PhpOffice\PhpPresentation\Reader;
21
22use DateTime;
23use DOMElement;
24use PhpOffice\Common\Drawing as CommonDrawing;
25use PhpOffice\Common\XMLReader;
26use PhpOffice\PhpPresentation\DocumentProperties;
27use PhpOffice\PhpPresentation\Exception\FileNotFoundException;
28use PhpOffice\PhpPresentation\Exception\InvalidFileFormatException;
29use PhpOffice\PhpPresentation\PhpPresentation;
30use PhpOffice\PhpPresentation\PresentationProperties;
31use PhpOffice\PhpPresentation\Shape\Drawing\Base64;
32use PhpOffice\PhpPresentation\Shape\Drawing\Gd;
33use PhpOffice\PhpPresentation\Shape\RichText;
34use PhpOffice\PhpPresentation\Shape\RichText\Paragraph;
35use PhpOffice\PhpPresentation\Slide\Background\Color as BackgroundColor;
36use PhpOffice\PhpPresentation\Slide\Background\Image;
37use PhpOffice\PhpPresentation\Style\Alignment;
38use PhpOffice\PhpPresentation\Style\Bullet;
39use PhpOffice\PhpPresentation\Style\Color;
40use PhpOffice\PhpPresentation\Style\Fill;
41use PhpOffice\PhpPresentation\Style\Font;
42use PhpOffice\PhpPresentation\Style\Shadow;
43use ZipArchive;
44
45/**
46 * Serialized format reader.
47 */
48class ODPresentation implements ReaderInterface
49{
50    /**
51     * Output Object.
52     *
53     * @var PhpPresentation
54     */
55    protected $oPhpPresentation;
56
57    /**
58     * Output Object.
59     *
60     * @var ZipArchive
61     */
62    protected $oZip;
63
64    /**
65     * @var array<string, array{alignment: null|Alignment, background: null, shadow: null|Shadow, fill: null|Fill, spacingAfter: null|int, spacingBefore: null|int, lineSpacingMode: null, lineSpacing: null, font: null, listStyle: null}>
66     */
67    protected $arrayStyles = [];
68
69    /**
70     * @var array<string, array<string, null|string>>
71     */
72    protected $arrayCommonStyles = [];
73
74    /**
75     * @var XMLReader
76     */
77    protected $oXMLReader;
78
79    /**
80     * @var int
81     */
82    protected $levelParagraph = 0;
83
84    /**
85     * Can the current \PhpOffice\PhpPresentation\Reader\ReaderInterface read the file?
86     */
87    public function canRead(string $pFilename): bool
88    {
89        return $this->fileSupportsUnserializePhpPresentation($pFilename);
90    }
91
92    /**
93     * Does a file support UnserializePhpPresentation ?
94     */
95    public function fileSupportsUnserializePhpPresentation(string $pFilename = ''): bool
96    {
97        // Check if file exists
98        if (!file_exists($pFilename)) {
99            throw new FileNotFoundException($pFilename);
100        }
101
102        $oZip = new ZipArchive();
103        // Is it a zip ?
104        if (true === $oZip->open($pFilename)) {
105            // Is it an OpenXML Document ?
106            // Is it a Presentation ?
107            if (is_array($oZip->statName('META-INF/manifest.xml')) && is_array($oZip->statName('mimetype')) && 'application/vnd.oasis.opendocument.presentation' == $oZip->getFromName('mimetype')) {
108                return true;
109            }
110        }
111
112        return false;
113    }
114
115    /**
116     * Loads PhpPresentation Serialized file.
117     */
118    public function load(string $pFilename): PhpPresentation
119    {
120        // Unserialize... First make sure the file supports it!
121        if (!$this->fileSupportsUnserializePhpPresentation($pFilename)) {
122            throw new InvalidFileFormatException($pFilename, self::class);
123        }
124
125        return $this->loadFile($pFilename);
126    }
127
128    /**
129     * Load PhpPresentation Serialized file.
130     *
131     * @param string $pFilename
132     *
133     * @return PhpPresentation
134     */
135    protected function loadFile($pFilename)
136    {
137        $this->oPhpPresentation = new PhpPresentation();
138        $this->oPhpPresentation->removeSlideByIndex();
139
140        $this->oZip = new ZipArchive();
141        $this->oZip->open($pFilename);
142
143        $this->oXMLReader = new XMLReader();
144        if (false !== $this->oXMLReader->getDomFromZip($pFilename, 'meta.xml')) {
145            $this->loadDocumentProperties();
146        }
147        $this->oXMLReader = new XMLReader();
148        if (false !== $this->oXMLReader->getDomFromZip($pFilename, 'styles.xml')) {
149            $this->loadStylesFile();
150        }
151        $this->oXMLReader = new XMLReader();
152        if (false !== $this->oXMLReader->getDomFromZip($pFilename, 'content.xml')) {
153            $this->loadSlides();
154            $this->loadPresentationProperties();
155        }
156
157        return $this->oPhpPresentation;
158    }
159
160    /**
161     * Read Document Properties.
162     */
163    protected function loadDocumentProperties(): void
164    {
165        $arrayProperties = [
166            '/office:document-meta/office:meta/meta:initial-creator' => 'setCreator',
167            '/office:document-meta/office:meta/dc:creator' => 'setLastModifiedBy',
168            '/office:document-meta/office:meta/dc:title' => 'setTitle',
169            '/office:document-meta/office:meta/dc:description' => 'setDescription',
170            '/office:document-meta/office:meta/dc:subject' => 'setSubject',
171            '/office:document-meta/office:meta/meta:keyword' => 'setKeywords',
172            '/office:document-meta/office:meta/meta:creation-date' => 'setCreated',
173            '/office:document-meta/office:meta/dc:date' => 'setModified',
174        ];
175        $properties = $this->oPhpPresentation->getDocumentProperties();
176        foreach ($arrayProperties as $path => $property) {
177            $oElement = $this->oXMLReader->getElement($path);
178            if ($oElement instanceof DOMElement) {
179                $value = $oElement->nodeValue;
180                if (in_array($property, ['setCreated', 'setModified'])) {
181                    $dateTime = DateTime::createFromFormat(DateTime::W3C, $value);
182                    if (!$dateTime) {
183                        $dateTime = new DateTime();
184                    }
185                    $value = $dateTime->getTimestamp();
186                }
187                $properties->{$property}($value);
188            }
189        }
190
191        foreach ($this->oXMLReader->getElements('/office:document-meta/office:meta/meta:user-defined') as $element) {
192            if (!($element instanceof DOMElement)
193                || !$element->hasAttribute('meta:name')) {
194                continue;
195            }
196            $propertyName = $element->getAttribute('meta:name');
197            $propertyValue = (string) $element->nodeValue;
198            $propertyType = $element->getAttribute('meta:value-type');
199            switch ($propertyType) {
200                case 'boolean':
201                    $propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN;
202
203                    break;
204                case 'float':
205                    $propertyType = filter_var($propertyValue, FILTER_VALIDATE_INT) === false
206                        ? DocumentProperties::PROPERTY_TYPE_FLOAT
207                        : DocumentProperties::PROPERTY_TYPE_INTEGER;
208
209                    break;
210                case 'date':
211                    $propertyType = DocumentProperties::PROPERTY_TYPE_DATE;
212
213                    break;
214                case 'string':
215                default:
216                    $propertyType = DocumentProperties::PROPERTY_TYPE_STRING;
217
218                    break;
219            }
220            $properties->setCustomProperty($propertyName, $propertyValue, $propertyType);
221        }
222    }
223
224    /**
225     * Extract all slides.
226     */
227    protected function loadSlides(): void
228    {
229        foreach ($this->oXMLReader->getElements('/office:document-content/office:automatic-styles/*') as $oElement) {
230            if ($oElement instanceof DOMElement && $oElement->hasAttribute('style:name')) {
231                $this->loadStyle($oElement);
232            }
233        }
234        foreach ($this->oXMLReader->getElements('/office:document-content/office:body/office:presentation/draw:page') as $oElement) {
235            if ($oElement instanceof DOMElement && 'draw:page' == $oElement->nodeName) {
236                $this->loadSlide($oElement);
237            }
238        }
239    }
240
241    protected function loadPresentationProperties(): void
242    {
243        $element = $this->oXMLReader->getElement('/office:document-content/office:body/office:presentation/presentation:settings');
244        if ($element instanceof DOMElement) {
245            if ($element->getAttribute('presentation:full-screen') === 'false') {
246                $this->oPhpPresentation->getPresentationProperties()->setSlideshowType(PresentationProperties::SLIDESHOW_TYPE_BROWSE);
247            }
248        }
249    }
250
251    /**
252     * Extract style.
253     */
254    protected function loadStyle(DOMElement $nodeStyle): bool
255    {
256        $keyStyle = $nodeStyle->getAttribute('style:name');
257
258        $nodeDrawingPageProps = $this->oXMLReader->getElement('style:drawing-page-properties', $nodeStyle);
259        if ($nodeDrawingPageProps instanceof DOMElement) {
260            // Read Background Color
261            if ($nodeDrawingPageProps->hasAttribute('draw:fill-color') && 'solid' == $nodeDrawingPageProps->getAttribute('draw:fill')) {
262                $oBackground = new BackgroundColor();
263                $oColor = new Color();
264                $oColor->setRGB(substr($nodeDrawingPageProps->getAttribute('draw:fill-color'), -6));
265                $oBackground->setColor($oColor);
266            }
267            // Read Background Image
268            if ('bitmap' == $nodeDrawingPageProps->getAttribute('draw:fill') && $nodeDrawingPageProps->hasAttribute('draw:fill-image-name')) {
269                $nameStyle = $nodeDrawingPageProps->getAttribute('draw:fill-image-name');
270                if (!empty($this->arrayCommonStyles[$nameStyle]) && 'image' == $this->arrayCommonStyles[$nameStyle]['type'] && !empty($this->arrayCommonStyles[$nameStyle]['path'])) {
271                    $tmpBkgImg = tempnam(sys_get_temp_dir(), 'PhpPresentationReaderODPBkg');
272                    $contentImg = $this->oZip->getFromName($this->arrayCommonStyles[$nameStyle]['path']);
273                    file_put_contents($tmpBkgImg, $contentImg);
274
275                    $oBackground = new Image();
276                    $oBackground->setPath($tmpBkgImg);
277                }
278            }
279        }
280
281        $nodeGraphicProps = $this->oXMLReader->getElement('style:graphic-properties', $nodeStyle);
282        if ($nodeGraphicProps instanceof DOMElement) {
283            // Read Shadow
284            if ($nodeGraphicProps->hasAttribute('draw:shadow') && 'visible' == $nodeGraphicProps->getAttribute('draw:shadow')) {
285                $oShadow = new Shadow();
286                $oShadow->setVisible(true);
287                if ($nodeGraphicProps->hasAttribute('draw:shadow-color')) {
288                    $oShadow->getColor()->setRGB(substr($nodeGraphicProps->getAttribute('draw:shadow-color'), -6));
289                }
290                if ($nodeGraphicProps->hasAttribute('draw:shadow-opacity')) {
291                    $oShadow->setAlpha(100 - (int) substr($nodeGraphicProps->getAttribute('draw:shadow-opacity'), 0, -1));
292                }
293                if ($nodeGraphicProps->hasAttribute('draw:shadow-offset-x') && $nodeGraphicProps->hasAttribute('draw:shadow-offset-y')) {
294                    $offsetX = (float) substr($nodeGraphicProps->getAttribute('draw:shadow-offset-x'), 0, -2);
295                    $offsetY = (float) substr($nodeGraphicProps->getAttribute('draw:shadow-offset-y'), 0, -2);
296                    $distance = 0;
297                    if (0 != $offsetX) {
298                        $distance = ($offsetX < 0 ? $offsetX * -1 : $offsetX);
299                    } elseif (0 != $offsetY) {
300                        $distance = ($offsetY < 0 ? $offsetY * -1 : $offsetY);
301                    }
302                    $oShadow->setDirection((int) rad2deg(atan2($offsetY, $offsetX)));
303                    $oShadow->setDistance(CommonDrawing::centimetersToPixels($distance));
304                }
305            }
306            // Read Fill
307            if ($nodeGraphicProps->hasAttribute('draw:fill')) {
308                $value = $nodeGraphicProps->getAttribute('draw:fill');
309
310                switch ($value) {
311                    case 'none':
312                        $oFill = new Fill();
313                        $oFill->setFillType(Fill::FILL_NONE);
314
315                        break;
316                    case 'solid':
317                        $oFill = new Fill();
318                        $oFill->setFillType(Fill::FILL_SOLID);
319                        if ($nodeGraphicProps->hasAttribute('draw:fill-color')) {
320                            $oColor = new Color();
321                            $oColor->setRGB(substr($nodeGraphicProps->getAttribute('draw:fill-color'), 1));
322                            $oFill->setStartColor($oColor);
323                        }
324
325                        break;
326                }
327            }
328        }
329
330        $nodeTextProperties = $this->oXMLReader->getElement('style:text-properties', $nodeStyle);
331        if ($nodeTextProperties instanceof DOMElement) {
332            $oFont = new Font();
333            if ($nodeTextProperties->hasAttribute('fo:color')) {
334                $oFont->getColor()->setRGB(substr($nodeTextProperties->getAttribute('fo:color'), -6));
335            }
336            if ($nodeTextProperties->hasAttribute('fo:text-transform')) {
337                switch ($nodeTextProperties->getAttribute('fo:text-transform')) {
338                    case 'none':
339                        $oFont->setCapitalization(Font::CAPITALIZATION_NONE);
340
341                        break;
342                    case 'lowercase':
343                        $oFont->setCapitalization(Font::CAPITALIZATION_SMALL);
344
345                        break;
346                    case 'uppercase':
347                        $oFont->setCapitalization(Font::CAPITALIZATION_ALL);
348
349                        break;
350                }
351            }
352            // Font Latin
353            if ($nodeTextProperties->hasAttribute('fo:font-family')) {
354                $oFont
355                    ->setName($nodeTextProperties->getAttribute('fo:font-family'))
356                    ->setFormat(Font::FORMAT_LATIN);
357            }
358            if ($nodeTextProperties->hasAttribute('fo:font-weight') && 'bold' == $nodeTextProperties->getAttribute('fo:font-weight')) {
359                $oFont
360                    ->setBold(true)
361                    ->setFormat(Font::FORMAT_LATIN);
362            }
363            if ($nodeTextProperties->hasAttribute('fo:font-size')) {
364                $oFont
365                    ->setSize((int) substr($nodeTextProperties->getAttribute('fo:font-size'), 0, -2))
366                    ->setFormat(Font::FORMAT_LATIN);
367            }
368            // Font East Asian
369            if ($nodeTextProperties->hasAttribute('style:font-family-asian')) {
370                $oFont
371                    ->setName($nodeTextProperties->getAttribute('style:font-family-asian'))
372                    ->setFormat(Font::FORMAT_EAST_ASIAN);
373            }
374            if ($nodeTextProperties->hasAttribute('style:font-weight-asian') && 'bold' == $nodeTextProperties->getAttribute('style:font-weight-asian')) {
375                $oFont
376                    ->setBold(true)
377                    ->setFormat(Font::FORMAT_EAST_ASIAN);
378            }
379            if ($nodeTextProperties->hasAttribute('style:font-size-asian')) {
380                $oFont
381                    ->setSize((int) substr($nodeTextProperties->getAttribute('style:font-size-asian'), 0, -2))
382                    ->setFormat(Font::FORMAT_EAST_ASIAN);
383            }
384            // Font Complex Script
385            if ($nodeTextProperties->hasAttribute('style:font-family-complex')) {
386                $oFont
387                    ->setName($nodeTextProperties->getAttribute('style:font-family-complex'))
388                    ->setFormat(Font::FORMAT_COMPLEX_SCRIPT);
389            }
390            if ($nodeTextProperties->hasAttribute('style:font-weight-complex') && 'bold' == $nodeTextProperties->getAttribute('style:font-weight-complex')) {
391                $oFont
392                    ->setBold(true)
393                    ->setFormat(Font::FORMAT_COMPLEX_SCRIPT);
394            }
395            if ($nodeTextProperties->hasAttribute('style:font-size-complex')) {
396                $oFont
397                    ->setSize((int) substr($nodeTextProperties->getAttribute('style:font-size-complex'), 0, -2))
398                    ->setFormat(Font::FORMAT_COMPLEX_SCRIPT);
399            }
400            if ($nodeTextProperties->hasAttribute('style:script-type')) {
401                switch ($nodeTextProperties->getAttribute('style:script-type')) {
402                    case 'latin':
403                        $oFont->setFormat(Font::FORMAT_LATIN);
404
405                        break;
406                    case 'asian':
407                        $oFont->setFormat(Font::FORMAT_EAST_ASIAN);
408
409                        break;
410                    case 'complex':
411                        $oFont->setFormat(Font::FORMAT_COMPLEX_SCRIPT);
412
413                        break;
414                }
415            }
416        }
417
418        $nodeParagraphProps = $this->oXMLReader->getElement('style:paragraph-properties', $nodeStyle);
419        if ($nodeParagraphProps instanceof DOMElement) {
420            if ($nodeParagraphProps->hasAttribute('fo:line-height')) {
421                $lineHeightUnit = $this->getExpressionUnit($nodeParagraphProps->getAttribute('fo:margin-bottom'));
422                $lineSpacingMode = $lineHeightUnit == '%' ? Paragraph::LINE_SPACING_MODE_PERCENT : Paragraph::LINE_SPACING_MODE_POINT;
423                $lineSpacing = $this->getExpressionValue($nodeParagraphProps->getAttribute('fo:margin-bottom'));
424            }
425            if ($nodeParagraphProps->hasAttribute('fo:margin-bottom')) {
426                $spacingAfter = (float) substr($nodeParagraphProps->getAttribute('fo:margin-bottom'), 0, -2);
427                $spacingAfter = CommonDrawing::centimetersToPoints($spacingAfter);
428            }
429            if ($nodeParagraphProps->hasAttribute('fo:margin-top')) {
430                $spacingBefore = (float) substr($nodeParagraphProps->getAttribute('fo:margin-top'), 0, -2);
431                $spacingBefore = CommonDrawing::centimetersToPoints($spacingBefore);
432            }
433            $oAlignment = new Alignment();
434            if ($nodeParagraphProps->hasAttribute('fo:text-align')) {
435                $oAlignment->setHorizontal($nodeParagraphProps->getAttribute('fo:text-align'));
436            }
437            if ($nodeParagraphProps->hasAttribute('style:writing-mode')) {
438                switch ($nodeParagraphProps->getAttribute('style:writing-mode')) {
439                    case 'lr-tb':
440                    case 'tb-lr':
441                    case 'lr':
442                        $oAlignment->setIsRTL(false);
443
444                        break;
445                    case 'rl-tb':
446                    case 'tb-rl':
447                    case 'rl':
448                        $oAlignment->setIsRTL(false);
449
450                        break;
451                    case 'tb':
452                    case 'page':
453                    default:
454                        break;
455                }
456            }
457        }
458
459        if ('text:list-style' == $nodeStyle->nodeName) {
460            $arrayListStyle = [];
461            foreach ($this->oXMLReader->getElements('text:list-level-style-bullet', $nodeStyle) as $oNodeListLevel) {
462                $oAlignment = new Alignment();
463                $oBullet = new Bullet();
464                $oBullet->setBulletType(Bullet::TYPE_NONE);
465                if ($oNodeListLevel instanceof DOMElement) {
466                    if ($oNodeListLevel->hasAttribute('text:level')) {
467                        $oAlignment->setLevel((int) $oNodeListLevel->getAttribute('text:level') - 1);
468                    }
469                    if ($oNodeListLevel->hasAttribute('text:bullet-char')) {
470                        $oBullet->setBulletChar($oNodeListLevel->getAttribute('text:bullet-char'));
471                        $oBullet->setBulletType(Bullet::TYPE_BULLET);
472                    }
473
474                    $oNodeListProperties = $this->oXMLReader->getElement('style:list-level-properties', $oNodeListLevel);
475                    if ($oNodeListProperties instanceof DOMElement) {
476                        if ($oNodeListProperties->hasAttribute('text:min-label-width')) {
477                            $oAlignment->setIndent(CommonDrawing::centimetersToPixels((float) substr($oNodeListProperties->getAttribute('text:min-label-width'), 0, -2)));
478                        }
479                        if ($oNodeListProperties->hasAttribute('text:space-before')) {
480                            $iSpaceBefore = CommonDrawing::centimetersToPixels((float) substr($oNodeListProperties->getAttribute('text:space-before'), 0, -2));
481                            $iMarginLeft = $iSpaceBefore + $oAlignment->getIndent();
482                            $oAlignment->setMarginLeft($iMarginLeft);
483                        }
484                    }
485
486                    $oNodeTextProperties = $this->oXMLReader->getElement('style:text-properties', $oNodeListLevel);
487                    if ($oNodeTextProperties instanceof DOMElement) {
488                        if ($oNodeTextProperties->hasAttribute('fo:font-family')) {
489                            $oBullet->setBulletFont($oNodeTextProperties->getAttribute('fo:font-family'));
490                        }
491                    }
492                }
493
494                $arrayListStyle[$oAlignment->getLevel()] = [
495                    'alignment' => $oAlignment,
496                    'bullet' => $oBullet,
497                ];
498            }
499        }
500
501        $this->arrayStyles[$keyStyle] = [
502            'alignment' => $oAlignment ?? null,
503            'background' => $oBackground ?? null,
504            'fill' => $oFill ?? null,
505            'font' => $oFont ?? null,
506            'shadow' => $oShadow ?? null,
507            'listStyle' => $arrayListStyle ?? null,
508            'spacingAfter' => $spacingAfter ?? null,
509            'spacingBefore' => $spacingBefore ?? null,
510            'lineSpacingMode' => $lineSpacingMode ?? null,
511            'lineSpacing' => $lineSpacing ?? null,
512        ];
513
514        return true;
515    }
516
517    /**
518     * Read Slide.
519     */
520    protected function loadSlide(DOMElement $nodeSlide): bool
521    {
522        // Core
523        $this->oPhpPresentation->createSlide();
524        $this->oPhpPresentation->setActiveSlideIndex($this->oPhpPresentation->getSlideCount() - 1);
525        if ($nodeSlide->hasAttribute('draw:name')) {
526            $this->oPhpPresentation->getActiveSlide()->setName($nodeSlide->getAttribute('draw:name'));
527        }
528        if ($nodeSlide->hasAttribute('draw:style-name')) {
529            $keyStyle = $nodeSlide->getAttribute('draw:style-name');
530            if (isset($this->arrayStyles[$keyStyle])) {
531                $this->oPhpPresentation->getActiveSlide()->setBackground($this->arrayStyles[$keyStyle]['background']);
532            }
533        }
534        foreach ($this->oXMLReader->getElements('draw:frame', $nodeSlide) as $oNodeFrame) {
535            if ($oNodeFrame instanceof DOMElement) {
536                if ($this->oXMLReader->getElement('draw:image', $oNodeFrame)) {
537                    $this->loadShapeDrawing($oNodeFrame);
538
539                    continue;
540                }
541                if ($this->oXMLReader->getElement('draw:text-box', $oNodeFrame)) {
542                    $this->loadShapeRichText($oNodeFrame);
543
544                    continue;
545                }
546            }
547        }
548
549        return true;
550    }
551
552    /**
553     * Read Shape Drawing.
554     */
555    protected function loadShapeDrawing(DOMElement $oNodeFrame): void
556    {
557        // Core
558        $mimetype = '';
559
560        $oNodeImage = $this->oXMLReader->getElement('draw:image', $oNodeFrame);
561        if ($oNodeImage instanceof DOMElement) {
562            if ($oNodeImage->hasAttribute('loext:mime-type')) {
563                $mimetype = $oNodeImage->getAttribute('loext:mime-type');
564            }
565            if ($oNodeImage->hasAttribute('xlink:href')) {
566                $sFilename = $oNodeImage->getAttribute('xlink:href');
567                // svm = StarView Metafile
568                if ('svm' == pathinfo($sFilename, PATHINFO_EXTENSION)) {
569                    return;
570                }
571                $imageFile = $this->oZip->getFromName($sFilename);
572            }
573        }
574
575        if (empty($imageFile)) {
576            return;
577        }
578
579        // Contents of file
580        if (empty($mimetype)) {
581            $shape = new Gd();
582            $shape->setImageResource(imagecreatefromstring($imageFile));
583        } else {
584            $shape = new Base64();
585            $shape->setData('data:' . $mimetype . ';base64,' . base64_encode($imageFile));
586        }
587
588        $shape->getShadow()->setVisible(false);
589        $shape->setName($oNodeFrame->hasAttribute('draw:name') ? $oNodeFrame->getAttribute('draw:name') : '');
590        $shape->setDescription($oNodeFrame->hasAttribute('draw:name') ? $oNodeFrame->getAttribute('draw:name') : '');
591        $shape->setResizeProportional(false);
592        $shape->setWidth($oNodeFrame->hasAttribute('svg:width') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:width'), 0, -2)) : 0);
593        $shape->setHeight($oNodeFrame->hasAttribute('svg:height') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:height'), 0, -2)) : 0);
594        $shape->setResizeProportional(true);
595        $shape->setOffsetX($oNodeFrame->hasAttribute('svg:x') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:x'), 0, -2)) : 0);
596        $shape->setOffsetY($oNodeFrame->hasAttribute('svg:y') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:y'), 0, -2)) : 0);
597
598        if ($oNodeFrame->hasAttribute('draw:style-name')) {
599            $keyStyle = $oNodeFrame->getAttribute('draw:style-name');
600            if (isset($this->arrayStyles[$keyStyle])) {
601                $shape->setShadow($this->arrayStyles[$keyStyle]['shadow']);
602                $shape->setFill($this->arrayStyles[$keyStyle]['fill']);
603            }
604        }
605
606        $this->oPhpPresentation->getActiveSlide()->addShape($shape);
607    }
608
609    /**
610     * Read Shape RichText.
611     */
612    protected function loadShapeRichText(DOMElement $oNodeFrame): void
613    {
614        // Core
615        $oShape = $this->oPhpPresentation->getActiveSlide()->createRichTextShape();
616        $oShape->setParagraphs([]);
617
618        $oShape->setWidth($oNodeFrame->hasAttribute('svg:width') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:width'), 0, -2)) : 0);
619        $oShape->setHeight($oNodeFrame->hasAttribute('svg:height') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:height'), 0, -2)) : 0);
620        $oShape->setOffsetX($oNodeFrame->hasAttribute('svg:x') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:x'), 0, -2)) : 0);
621        $oShape->setOffsetY($oNodeFrame->hasAttribute('svg:y') ? CommonDrawing::centimetersToPixels((float) substr($oNodeFrame->getAttribute('svg:y'), 0, -2)) : 0);
622
623        foreach ($this->oXMLReader->getElements('draw:text-box/*', $oNodeFrame) as $oNodeParagraph) {
624            $this->levelParagraph = 0;
625            if ($oNodeParagraph instanceof DOMElement) {
626                if ('text:p' == $oNodeParagraph->nodeName) {
627                    $this->readParagraph($oShape, $oNodeParagraph);
628                }
629                if ('text:list' == $oNodeParagraph->nodeName) {
630                    $this->readList($oShape, $oNodeParagraph);
631                }
632            }
633        }
634
635        if (count($oShape->getParagraphs()) > 0) {
636            $oShape->setActiveParagraph(0);
637        }
638    }
639
640    /**
641     * Read Paragraph.
642     */
643    protected function readParagraph(RichText $oShape, DOMElement $oNodeParent): void
644    {
645        $oParagraph = $oShape->createParagraph();
646        if ($oNodeParent->hasAttribute('text:style-name')) {
647            $keyStyle = $oNodeParent->getAttribute('text:style-name');
648            if (isset($this->arrayStyles[$keyStyle])) {
649                if (!empty($this->arrayStyles[$keyStyle]['spacingAfter'])) {
650                    $oParagraph->setSpacingAfter($this->arrayStyles[$keyStyle]['spacingAfter']);
651                }
652                if (!empty($this->arrayStyles[$keyStyle]['spacingBefore'])) {
653                    $oParagraph->setSpacingBefore($this->arrayStyles[$keyStyle]['spacingBefore']);
654                }
655                if (!empty($this->arrayStyles[$keyStyle]['lineSpacingMode'])) {
656                    $oParagraph->setLineSpacingMode($this->arrayStyles[$keyStyle]['lineSpacingMode']);
657                }
658                if (!empty($this->arrayStyles[$keyStyle]['lineSpacing'])) {
659                    $oParagraph->setLineSpacing($this->arrayStyles[$keyStyle]['lineSpacing']);
660                }
661            }
662        }
663        $oDomList = $this->oXMLReader->getElements('text:span', $oNodeParent);
664        $oDomTextNodes = $this->oXMLReader->getElements('text()', $oNodeParent);
665        foreach ($oDomTextNodes as $oDomTextNode) {
666            if ('' != trim($oDomTextNode->nodeValue)) {
667                $oTextRun = $oParagraph->createTextRun();
668                $oTextRun->setText(trim($oDomTextNode->nodeValue));
669            }
670        }
671        foreach ($oDomList as $oNodeRichTextElement) {
672            if ($oNodeRichTextElement instanceof DOMElement) {
673                $this->readParagraphItem($oParagraph, $oNodeRichTextElement);
674            }
675        }
676    }
677
678    /**
679     * Read Paragraph Item.
680     */
681    protected function readParagraphItem(Paragraph $oParagraph, DOMElement $oNodeParent): void
682    {
683        if ($this->oXMLReader->elementExists('text:line-break', $oNodeParent)) {
684            $oParagraph->createBreak();
685        } else {
686            $oTextRun = $oParagraph->createTextRun();
687            if ($oNodeParent->hasAttribute('text:style-name')) {
688                $keyStyle = $oNodeParent->getAttribute('text:style-name');
689                if (isset($this->arrayStyles[$keyStyle])) {
690                    $oTextRun->setFont($this->arrayStyles[$keyStyle]['font']);
691                }
692            }
693            $oTextRunLink = $this->oXMLReader->getElement('text:a', $oNodeParent);
694            if ($oTextRunLink instanceof DOMElement) {
695                $oTextRun->setText($oTextRunLink->nodeValue);
696                if ($oTextRunLink->hasAttribute('xlink:href')) {
697                    $oTextRun->getHyperlink()->setUrl($oTextRunLink->getAttribute('xlink:href'));
698                }
699            } else {
700                $oTextRun->setText($oNodeParent->nodeValue);
701            }
702        }
703    }
704
705    /**
706     * Read List.
707     */
708    protected function readList(RichText $oShape, DOMElement $oNodeParent): void
709    {
710        foreach ($this->oXMLReader->getElements('text:list-item/*', $oNodeParent) as $oNodeListItem) {
711            if ($oNodeListItem instanceof DOMElement) {
712                if ('text:p' == $oNodeListItem->nodeName) {
713                    $this->readListItem($oShape, $oNodeListItem, $oNodeParent);
714                }
715                if ('text:list' == $oNodeListItem->nodeName) {
716                    ++$this->levelParagraph;
717                    $this->readList($oShape, $oNodeListItem);
718                    --$this->levelParagraph;
719                }
720            }
721        }
722    }
723
724    /**
725     * Read List Item.
726     */
727    protected function readListItem(RichText $oShape, DOMElement $oNodeParent, DOMElement $oNodeParagraph): void
728    {
729        $oParagraph = $oShape->createParagraph();
730        if ($oNodeParagraph->hasAttribute('text:style-name')) {
731            $keyStyle = $oNodeParagraph->getAttribute('text:style-name');
732            if (isset($this->arrayStyles[$keyStyle]) && !empty($this->arrayStyles[$keyStyle]['listStyle'])) {
733                $oParagraph->setAlignment($this->arrayStyles[$keyStyle]['listStyle'][$this->levelParagraph]['alignment']);
734                $oParagraph->setBulletStyle($this->arrayStyles[$keyStyle]['listStyle'][$this->levelParagraph]['bullet']);
735            }
736        }
737        foreach ($this->oXMLReader->getElements('text:span', $oNodeParent) as $oNodeRichTextElement) {
738            if ($oNodeRichTextElement instanceof DOMElement) {
739                $this->readParagraphItem($oParagraph, $oNodeRichTextElement);
740            }
741        }
742    }
743
744    /**
745     * Load file 'styles.xml'.
746     */
747    protected function loadStylesFile(): void
748    {
749        foreach ($this->oXMLReader->getElements('/office:document-styles/office:styles/*') as $oElement) {
750            if ($oElement instanceof DOMElement && 'draw:fill-image' == $oElement->nodeName) {
751                $this->arrayCommonStyles[$oElement->getAttribute('draw:name')] = [
752                    'type' => 'image',
753                    'path' => $oElement->hasAttribute('xlink:href') ? $oElement->getAttribute('xlink:href') : null,
754                ];
755            }
756        }
757    }
758
759    private function getExpressionUnit(string $expr): string
760    {
761        if (substr($expr, -1) == '%') {
762            return '%';
763        }
764
765        return substr($expr, -2);
766    }
767
768    private function getExpressionValue(string $expr): string
769    {
770        if (substr($expr, -1) == '%') {
771            return substr($expr, 0, -1);
772        }
773
774        return substr($expr, 0, -2);
775    }
776}