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