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