Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.95% covered (success)
96.95%
159 / 164
89.29% covered (warning)
89.29%
25 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
Image
96.95% covered (success)
96.95%
159 / 164
89.29% covered (warning)
89.29%
25 / 28
70
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
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 null|string
73     */
74    private $name;
75
76    /**
77     * Image alt text.
78     *
79     * @var null|string
80     */
81    private $altText;
82
83    /**
84     * Image type.
85     *
86     * @var string
87     */
88    private $imageType;
89
90    /**
91     * Image create function.
92     *
93     * @var string
94     */
95    private $imageCreateFunc;
96
97    /**
98     * Image function.
99     *
100     * @var null|callable(resource): void
101     */
102    private $imageFunc;
103
104    /**
105     * Image extension.
106     *
107     * @var string
108     */
109    private $imageExtension;
110
111    /**
112     * Image quality.
113     *
114     * Functions imagepng() and imagejpeg() have an optional parameter for
115     * quality.
116     *
117     * @var null|int
118     */
119    private $imageQuality;
120
121    /**
122     * Is memory image.
123     *
124     * @var bool
125     */
126    private $memoryImage;
127
128    /**
129     * Image target file name.
130     *
131     * @var string
132     */
133    private $target;
134
135    /**
136     * Image media index.
137     *
138     * @var int
139     */
140    private $mediaIndex;
141
142    /**
143     * Has media relation flag; true for Link, Image, and Object.
144     *
145     * @var bool
146     */
147    protected $mediaRelation = true;
148
149    /**
150     * Create new image element.
151     *
152     * @param string $source
153     * @param mixed $style
154     * @param bool $watermark
155     * @param null|string $name
156     * @param null|string $altText
157     */
158    public function __construct($source, $style = null, $watermark = false, $name = null, $altText = null)
159    {
160        $this->source = $source;
161        $this->style = $this->setNewStyle(new ImageStyle(), $style, true);
162        $this->setIsWatermark($watermark);
163        $this->setName($name);
164        $this->setAltText($altText);
165
166        $this->checkImage();
167    }
168
169    /**
170     * Get Image style.
171     *
172     * @return ?ImageStyle
173     */
174    public function getStyle()
175    {
176        return $this->style;
177    }
178
179    /**
180     * Get image source.
181     *
182     * @return string
183     */
184    public function getSource()
185    {
186        return $this->source;
187    }
188
189    /**
190     * Get image source type.
191     *
192     * @return string
193     */
194    public function getSourceType()
195    {
196        return $this->sourceType;
197    }
198
199    /**
200     * Sets the image name.
201     *
202     * @param null|string $value
203     */
204    public function setName($value): void
205    {
206        $this->name = $value;
207    }
208
209    /**
210     * Get image alt text.
211     *
212     * @return null|string
213     */
214    public function getAltText(): ?string 
215    {
216        return $this->altText;
217    }
218
219    /**
220     * Sets the image alt text.
221     *
222     * @param null|string $value
223     */
224    public function setAltText(?string $value): void
225    {
226        $this->altText = $value;
227    }
228
229    /**
230     * Get image name.
231     *
232     * @return null|string
233     */
234    public function getName()
235    {
236        return $this->name;
237    }
238
239    /**
240     * Get image media ID.
241     *
242     * @return string
243     */
244    public function getMediaId()
245    {
246        return md5($this->source);
247    }
248
249    /**
250     * Get is watermark.
251     *
252     * @return bool
253     */
254    public function isWatermark()
255    {
256        return $this->watermark;
257    }
258
259    /**
260     * Set is watermark.
261     *
262     * @param bool $value
263     */
264    public function setIsWatermark($value): void
265    {
266        $this->watermark = $value;
267    }
268
269    /**
270     * Get image type.
271     *
272     * @return string
273     */
274    public function getImageType()
275    {
276        return $this->imageType;
277    }
278
279    /**
280     * Get image create function.
281     *
282     * @return string
283     */
284    public function getImageCreateFunction()
285    {
286        return $this->imageCreateFunc;
287    }
288
289    /**
290     * Get image function.
291     *
292     * @return null|callable(resource): void
293     */
294    public function getImageFunction(): ?callable
295    {
296        return $this->imageFunc;
297    }
298
299    /**
300     * Get image quality.
301     */
302    public function getImageQuality(): ?int
303    {
304        return $this->imageQuality;
305    }
306
307    /**
308     * Get image extension.
309     *
310     * @return string
311     */
312    public function getImageExtension()
313    {
314        return $this->imageExtension;
315    }
316
317    /**
318     * Get is memory image.
319     *
320     * @return bool
321     */
322    public function isMemImage()
323    {
324        return $this->memoryImage;
325    }
326
327    /**
328     * Get target file name.
329     *
330     * @return string
331     */
332    public function getTarget()
333    {
334        return $this->target;
335    }
336
337    /**
338     * Set target file name.
339     *
340     * @param string $value
341     */
342    public function setTarget($value): void
343    {
344        $this->target = $value;
345    }
346
347    /**
348     * Get media index.
349     *
350     * @return int
351     */
352    public function getMediaIndex()
353    {
354        return $this->mediaIndex;
355    }
356
357    /**
358     * Set media index.
359     *
360     * @param int $value
361     */
362    public function setMediaIndex($value): void
363    {
364        $this->mediaIndex = $value;
365    }
366
367    /**
368     * Get image string.
369     */
370    public function getImageString(): ?string
371    {
372        $source = $this->source;
373        $actualSource = null;
374        $imageBinary = null;
375        $isTemp = false;
376
377        // Get actual source from archive image or other source
378        // Return null if not found
379        if ($this->sourceType == self::SOURCE_ARCHIVE) {
380            $source = substr($source, 6);
381            [$zipFilename, $imageFilename] = explode('#', $source);
382
383            $zip = new ZipArchive();
384            if ($zip->open($zipFilename) !== false) {
385                if ($zip->locateName($imageFilename) !== false) {
386                    $isTemp = true;
387                    $zip->extractTo(Settings::getTempDir(), $imageFilename);
388                    $actualSource = Settings::getTempDir() . DIRECTORY_SEPARATOR . $imageFilename;
389                }
390            }
391            $zip->close();
392        } else {
393            $actualSource = $source;
394        }
395
396        // Can't find any case where $actualSource = null hasn't captured by
397        // preceding exceptions. Please uncomment when you find the case and
398        // put the case into Element\ImageTest.
399        // if ($actualSource === null) {
400        //     return null;
401        // }
402
403        // Read image binary data and convert to hex/base64 string
404        if ($this->sourceType == self::SOURCE_GD) {
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}