Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.89% covered (success)
96.89%
156 / 161
88.46% covered (warning)
88.46%
23 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Image
96.89% covered (success)
96.89%
156 / 161
88.46% covered (warning)
88.46%
23 / 26
68
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSourceType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMediaId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWatermark
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIsWatermark
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageCreateFunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageFunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageQuality
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageExtension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMemImage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMediaIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMediaIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageString
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
10
 getImageStringData
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 checkImage
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 setSourceType
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
7.23
 getArchiveImageSize
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 setFunctions
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
11
 setProportionalSize
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3/**
4 * This file is part of PHPWord - A pure PHP library for reading and writing
5 * word processing documents.
6 *
7 * PHPWord is free software distributed under the terms of the GNU Lesser
8 * General Public License version 3 as published by the Free Software Foundation.
9 *
10 * For the full copyright and license information, please read the LICENSE
11 * file that was distributed with this source code. For the full list of
12 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
13 *
14 * @see         https://github.com/PHPOffice/PHPWord
15 *
16 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
17 */
18
19namespace PhpOffice\PhpWord\Element;
20
21use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
22use PhpOffice\PhpWord\Exception\InvalidImageException;
23use PhpOffice\PhpWord\Exception\UnsupportedImageTypeException;
24use PhpOffice\PhpWord\Settings;
25use PhpOffice\PhpWord\Shared\ZipArchive;
26use PhpOffice\PhpWord\Style\Image as ImageStyle;
27
28/**
29 * Image element.
30 */
31class Image extends AbstractElement
32{
33    /**
34     * Image source type constants.
35     */
36    const SOURCE_LOCAL = 'local'; // Local images
37    const SOURCE_GD = 'gd'; // Generated using GD
38    const SOURCE_ARCHIVE = 'archive'; // Image in archives zip://$archive#$image
39    const SOURCE_STRING = 'string'; // Image from string
40
41    /**
42     * Image source.
43     *
44     * @var string
45     */
46    private $source;
47
48    /**
49     * Source type: local|gd|archive.
50     *
51     * @var string
52     */
53    private $sourceType;
54
55    /**
56     * Image style.
57     *
58     * @var ?ImageStyle
59     */
60    private $style;
61
62    /**
63     * Is watermark.
64     *
65     * @var bool
66     */
67    private $watermark;
68
69    /**
70     * Name of image.
71     *
72     * @var string
73     */
74    private $name;
75
76    /**
77     * Image type.
78     *
79     * @var string
80     */
81    private $imageType;
82
83    /**
84     * Image create function.
85     *
86     * @var string
87     */
88    private $imageCreateFunc;
89
90    /**
91     * Image function.
92     *
93     * @var null|callable(resource): void
94     */
95    private $imageFunc;
96
97    /**
98     * Image extension.
99     *
100     * @var string
101     */
102    private $imageExtension;
103
104    /**
105     * Image quality.
106     *
107     * Functions imagepng() and imagejpeg() have an optional parameter for
108     * quality.
109     *
110     * @var null|int
111     */
112    private $imageQuality;
113
114    /**
115     * Is memory image.
116     *
117     * @var bool
118     */
119    private $memoryImage;
120
121    /**
122     * Image target file name.
123     *
124     * @var string
125     */
126    private $target;
127
128    /**
129     * Image media index.
130     *
131     * @var int
132     */
133    private $mediaIndex;
134
135    /**
136     * Has media relation flag; true for Link, Image, and Object.
137     *
138     * @var bool
139     */
140    protected $mediaRelation = true;
141
142    /**
143     * Create new image element.
144     *
145     * @param string $source
146     * @param mixed $style
147     * @param bool $watermark
148     * @param string $name
149     */
150    public function __construct($source, $style = null, $watermark = false, $name = null)
151    {
152        $this->source = $source;
153        $this->style = $this->setNewStyle(new ImageStyle(), $style, true);
154        $this->setIsWatermark($watermark);
155        $this->setName($name);
156
157        $this->checkImage();
158    }
159
160    /**
161     * Get Image style.
162     *
163     * @return ?ImageStyle
164     */
165    public function getStyle()
166    {
167        return $this->style;
168    }
169
170    /**
171     * Get image source.
172     *
173     * @return string
174     */
175    public function getSource()
176    {
177        return $this->source;
178    }
179
180    /**
181     * Get image source type.
182     *
183     * @return string
184     */
185    public function getSourceType()
186    {
187        return $this->sourceType;
188    }
189
190    /**
191     * Sets the image name.
192     *
193     * @param string $value
194     */
195    public function setName($value): void
196    {
197        $this->name = $value;
198    }
199
200    /**
201     * Get image name.
202     *
203     * @return null|string
204     */
205    public function getName()
206    {
207        return $this->name;
208    }
209
210    /**
211     * Get image media ID.
212     *
213     * @return string
214     */
215    public function getMediaId()
216    {
217        return md5($this->source);
218    }
219
220    /**
221     * Get is watermark.
222     *
223     * @return bool
224     */
225    public function isWatermark()
226    {
227        return $this->watermark;
228    }
229
230    /**
231     * Set is watermark.
232     *
233     * @param bool $value
234     */
235    public function setIsWatermark($value): void
236    {
237        $this->watermark = $value;
238    }
239
240    /**
241     * Get image type.
242     *
243     * @return string
244     */
245    public function getImageType()
246    {
247        return $this->imageType;
248    }
249
250    /**
251     * Get image create function.
252     *
253     * @return string
254     */
255    public function getImageCreateFunction()
256    {
257        return $this->imageCreateFunc;
258    }
259
260    /**
261     * Get image function.
262     *
263     * @return null|callable(resource): void
264     */
265    public function getImageFunction(): ?callable
266    {
267        return $this->imageFunc;
268    }
269
270    /**
271     * Get image quality.
272     */
273    public function getImageQuality(): ?int
274    {
275        return $this->imageQuality;
276    }
277
278    /**
279     * Get image extension.
280     *
281     * @return string
282     */
283    public function getImageExtension()
284    {
285        return $this->imageExtension;
286    }
287
288    /**
289     * Get is memory image.
290     *
291     * @return bool
292     */
293    public function isMemImage()
294    {
295        return $this->memoryImage;
296    }
297
298    /**
299     * Get target file name.
300     *
301     * @return string
302     */
303    public function getTarget()
304    {
305        return $this->target;
306    }
307
308    /**
309     * Set target file name.
310     *
311     * @param string $value
312     */
313    public function setTarget($value): void
314    {
315        $this->target = $value;
316    }
317
318    /**
319     * Get media index.
320     *
321     * @return int
322     */
323    public function getMediaIndex()
324    {
325        return $this->mediaIndex;
326    }
327
328    /**
329     * Set media index.
330     *
331     * @param int $value
332     */
333    public function setMediaIndex($value): void
334    {
335        $this->mediaIndex = $value;
336    }
337
338    /**
339     * Get image string.
340     */
341    public function getImageString(): ?string
342    {
343        $source = $this->source;
344        $actualSource = null;
345        $imageBinary = null;
346        $isTemp = false;
347
348        // Get actual source from archive image or other source
349        // Return null if not found
350        if ($this->sourceType == self::SOURCE_ARCHIVE) {
351            $source = substr($source, 6);
352            [$zipFilename, $imageFilename] = explode('#', $source);
353
354            $zip = new ZipArchive();
355            if ($zip->open($zipFilename) !== false) {
356                if ($zip->locateName($imageFilename) !== false) {
357                    $isTemp = true;
358                    $zip->extractTo(Settings::getTempDir(), $imageFilename);
359                    $actualSource = Settings::getTempDir() . DIRECTORY_SEPARATOR . $imageFilename;
360                }
361            }
362            $zip->close();
363        } else {
364            $actualSource = $source;
365        }
366
367        // Can't find any case where $actualSource = null hasn't captured by
368        // preceding exceptions. Please uncomment when you find the case and
369        // put the case into Element\ImageTest.
370        // if ($actualSource === null) {
371        //     return null;
372        // }
373
374        // Read image binary data and convert to hex/base64 string
375        if ($this->sourceType == self::SOURCE_GD) {
376            $imageResource = call_user_func($this->imageCreateFunc, $actualSource);
377            if ($this->imageType === 'image/png') {
378                // PNG images need to preserve alpha channel information
379                imagesavealpha($imageResource, true);
380            }
381            ob_start();
382            $callback = $this->imageFunc;
383            $callback($imageResource);
384            $imageBinary = ob_get_contents();
385            ob_end_clean();
386        } elseif ($this->sourceType == self::SOURCE_STRING) {
387            $imageBinary = $this->source;
388        } else {
389            $fileHandle = fopen($actualSource, 'rb', false);
390            $fileSize = filesize($actualSource);
391            if ($fileHandle !== false && $fileSize > 0) {
392                $imageBinary = fread($fileHandle, $fileSize);
393                fclose($fileHandle);
394            }
395        }
396
397        // Delete temporary file if necessary
398        if ($isTemp === true) {
399            @unlink($actualSource);
400        }
401
402        return $imageBinary;
403    }
404
405    /**
406     * Get image string data.
407     *
408     * @param bool $base64
409     *
410     * @return null|string
411     *
412     * @since 0.11.0
413     */
414    public function getImageStringData($base64 = false)
415    {
416        $imageBinary = $this->getImageString();
417        if ($imageBinary === null) {
418            return null;
419        }
420
421        if ($base64) {
422            return base64_encode($imageBinary);
423        }
424
425        return bin2hex($imageBinary);
426    }
427
428    /**
429     * Check memory image, supported type, image functions, and proportional width/height.
430     */
431    private function checkImage(): void
432    {
433        $this->setSourceType();
434
435        // Check image data
436        if ($this->sourceType == self::SOURCE_ARCHIVE) {
437            $imageData = $this->getArchiveImageSize($this->source);
438        } elseif ($this->sourceType == self::SOURCE_STRING) {
439            $imageData = @getimagesizefromstring($this->source);
440        } else {
441            $imageData = @getimagesize($this->source);
442        }
443        if (!is_array($imageData)) {
444            throw new InvalidImageException(sprintf('Invalid image: %s', $this->source));
445        }
446        [$actualWidth, $actualHeight, $imageType] = $imageData;
447
448        // Check image type support
449        $supportedTypes = [IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG];
450        if ($this->sourceType != self::SOURCE_GD && $this->sourceType != self::SOURCE_STRING) {
451            $supportedTypes = array_merge($supportedTypes, [IMAGETYPE_BMP, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM]);
452        }
453        if (!in_array($imageType, $supportedTypes)) {
454            throw new UnsupportedImageTypeException();
455        }
456
457        // Define image functions
458        $this->imageType = image_type_to_mime_type($imageType);
459        $this->setFunctions();
460        $this->setProportionalSize($actualWidth, $actualHeight);
461    }
462
463    /**
464     * Set source type.
465     */
466    private function setSourceType(): void
467    {
468        if (stripos(strrev($this->source), strrev('.php')) === 0) {
469            $this->memoryImage = true;
470            $this->sourceType = self::SOURCE_GD;
471        } elseif (strpos($this->source, 'zip://') !== false) {
472            $this->memoryImage = false;
473            $this->sourceType = self::SOURCE_ARCHIVE;
474        } elseif (filter_var($this->source, FILTER_VALIDATE_URL) !== false) {
475            $this->memoryImage = true;
476            if (strpos($this->source, 'https') === 0) {
477                $fileContent = file_get_contents($this->source);
478                $this->source = $fileContent;
479                $this->sourceType = self::SOURCE_STRING;
480            } else {
481                $this->sourceType = self::SOURCE_GD;
482            }
483        } elseif ((strpos($this->source, chr(0)) === false) && @file_exists($this->source)) {
484            $this->memoryImage = false;
485            $this->sourceType = self::SOURCE_LOCAL;
486        } else {
487            $this->memoryImage = true;
488            $this->sourceType = self::SOURCE_STRING;
489        }
490    }
491
492    /**
493     * Get image size from archive.
494     *
495     * @since 0.12.0 Throws CreateTemporaryFileException.
496     *
497     * @param string $source
498     *
499     * @return null|array
500     */
501    private function getArchiveImageSize($source)
502    {
503        $imageData = null;
504        $source = substr($source, 6);
505        [$zipFilename, $imageFilename] = explode('#', $source);
506
507        $tempFilename = tempnam(Settings::getTempDir(), 'PHPWordImage');
508        if (false === $tempFilename) {
509            throw new CreateTemporaryFileException(); // @codeCoverageIgnore
510        }
511
512        $zip = new ZipArchive();
513        if ($zip->open($zipFilename) !== false) {
514            if ($zip->locateName($imageFilename) !== false) {
515                $imageContent = $zip->getFromName($imageFilename);
516                if ($imageContent !== false) {
517                    file_put_contents($tempFilename, $imageContent);
518                    $imageData = getimagesize($tempFilename);
519                    unlink($tempFilename);
520                }
521            }
522            $zip->close();
523        }
524
525        return $imageData;
526    }
527
528    /**
529     * Set image functions and extensions.
530     */
531    private function setFunctions(): void
532    {
533        switch ($this->imageType) {
534            case 'image/png':
535                $this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefrompng';
536                $this->imageFunc = function ($resource): void {
537                    imagepng($resource, null, $this->imageQuality);
538                };
539                $this->imageExtension = 'png';
540                $this->imageQuality = -1;
541
542                break;
543            case 'image/gif':
544                $this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefromgif';
545                $this->imageFunc = function ($resource): void {
546                    imagegif($resource);
547                };
548                $this->imageExtension = 'gif';
549                $this->imageQuality = null;
550
551                break;
552            case 'image/jpeg':
553            case 'image/jpg':
554                $this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefromjpeg';
555                $this->imageFunc = function ($resource): void {
556                    imagejpeg($resource, null, $this->imageQuality);
557                };
558                $this->imageExtension = 'jpg';
559                $this->imageQuality = 100;
560
561                break;
562            case 'image/bmp':
563            case 'image/x-ms-bmp':
564                $this->imageType = 'image/bmp';
565                $this->imageFunc = null;
566                $this->imageExtension = 'bmp';
567                $this->imageQuality = null;
568
569                break;
570            case 'image/tiff':
571                $this->imageType = 'image/tiff';
572                $this->imageFunc = null;
573                $this->imageExtension = 'tif';
574                $this->imageQuality = null;
575
576                break;
577        }
578    }
579
580    /**
581     * Set proportional width/height if one dimension not available.
582     *
583     * @param int $actualWidth
584     * @param int $actualHeight
585     */
586    private function setProportionalSize($actualWidth, $actualHeight): void
587    {
588        $styleWidth = $this->style->getWidth();
589        $styleHeight = $this->style->getHeight();
590        if (!($styleWidth && $styleHeight)) {
591            if ($styleWidth == null && $styleHeight == null) {
592                $this->style->setWidth($actualWidth);
593                $this->style->setHeight($actualHeight);
594            } elseif ($styleWidth) {
595                $this->style->setHeight($actualHeight * ($styleWidth / $actualWidth));
596            } else {
597                $this->style->setWidth($actualWidth * ($styleHeight / $actualHeight));
598            }
599        }
600    }
601}