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    public function getAltText(): ?string
213    {
214        return $this->altText;
215    }
216
217    /**
218     * Sets the image alt text.
219     */
220    public function setAltText(?string $value): void
221    {
222        $this->altText = $value;
223    }
224
225    /**
226     * Get image name.
227     *
228     * @return null|string
229     */
230    public function getName()
231    {
232        return $this->name;
233    }
234
235    /**
236     * Get image media ID.
237     *
238     * @return string
239     */
240    public function getMediaId()
241    {
242        return md5($this->source);
243    }
244
245    /**
246     * Get is watermark.
247     *
248     * @return bool
249     */
250    public function isWatermark()
251    {
252        return $this->watermark;
253    }
254
255    /**
256     * Set is watermark.
257     *
258     * @param bool $value
259     */
260    public function setIsWatermark($value): void
261    {
262        $this->watermark = $value;
263    }
264
265    /**
266     * Get image type.
267     *
268     * @return string
269     */
270    public function getImageType()
271    {
272        return $this->imageType;
273    }
274
275    /**
276     * Get image create function.
277     *
278     * @return string
279     */
280    public function getImageCreateFunction()
281    {
282        return $this->imageCreateFunc;
283    }
284
285    /**
286     * Get image function.
287     *
288     * @return null|callable(resource): void
289     */
290    public function getImageFunction(): ?callable
291    {
292        return $this->imageFunc;
293    }
294
295    /**
296     * Get image quality.
297     */
298    public function getImageQuality(): ?int
299    {
300        return $this->imageQuality;
301    }
302
303    /**
304     * Get image extension.
305     *
306     * @return string
307     */
308    public function getImageExtension()
309    {
310        return $this->imageExtension;
311    }
312
313    /**
314     * Get is memory image.
315     *
316     * @return bool
317     */
318    public function isMemImage()
319    {
320        return $this->memoryImage;
321    }
322
323    /**
324     * Get target file name.
325     *
326     * @return string
327     */
328    public function getTarget()
329    {
330        return $this->target;
331    }
332
333    /**
334     * Set target file name.
335     *
336     * @param string $value
337     */
338    public function setTarget($value): void
339    {
340        $this->target = $value;
341    }
342
343    /**
344     * Get media index.
345     *
346     * @return int
347     */
348    public function getMediaIndex()
349    {
350        return $this->mediaIndex;
351    }
352
353    /**
354     * Set media index.
355     *
356     * @param int $value
357     */
358    public function setMediaIndex($value): void
359    {
360        $this->mediaIndex = $value;
361    }
362
363    /**
364     * Get image string.
365     */
366    public function getImageString(): ?string
367    {
368        $source = $this->source;
369        $actualSource = null;
370        $imageBinary = null;
371        $isTemp = false;
372
373        // Get actual source from archive image or other source
374        // Return null if not found
375        if ($this->sourceType == self::SOURCE_ARCHIVE) {
376            $source = substr($source, 6);
377            [$zipFilename, $imageFilename] = explode('#', $source);
378
379            $zip = new ZipArchive();
380            if ($zip->open($zipFilename) !== false) {
381                if ($zip->locateName($imageFilename) !== false) {
382                    $isTemp = true;
383                    $zip->extractTo(Settings::getTempDir(), $imageFilename);
384                    $actualSource = Settings::getTempDir() . DIRECTORY_SEPARATOR . $imageFilename;
385                }
386            }
387            $zip->close();
388        } else {
389            $actualSource = $source;
390        }
391
392        // Can't find any case where $actualSource = null hasn't captured by
393        // preceding exceptions. Please uncomment when you find the case and
394        // put the case into Element\ImageTest.
395        // if ($actualSource === null) {
396        //     return null;
397        // }
398
399        // Read image binary data and convert to hex/base64 string
400        if ($this->sourceType == self::SOURCE_GD) {
401            $imageResource = call_user_func($this->imageCreateFunc, $actualSource);
402            if ($this->imageType === 'image/png') {
403                // PNG images need to preserve alpha channel information
404                imagesavealpha($imageResource, true);
405            }
406            ob_start();
407            $callback = $this->imageFunc;
408            $callback($imageResource);
409            $imageBinary = ob_get_contents();
410            ob_end_clean();
411        } elseif ($this->sourceType == self::SOURCE_STRING) {
412            $imageBinary = $this->source;
413        } else {
414            $fileHandle = fopen($actualSource, 'rb', false);
415            $fileSize = filesize($actualSource);
416            if ($fileHandle !== false && $fileSize > 0) {
417                $imageBinary = fread($fileHandle, $fileSize);
418                fclose($fileHandle);
419            }
420        }
421
422        // Delete temporary file if necessary
423        if ($isTemp === true) {
424            @unlink($actualSource);
425        }
426
427        return $imageBinary;
428    }
429
430    /**
431     * Get image string data.
432     *
433     * @param bool $base64
434     *
435     * @return null|string
436     *
437     * @since 0.11.0
438     */
439    public function getImageStringData($base64 = false)
440    {
441        $imageBinary = $this->getImageString();
442        if ($imageBinary === null) {
443            return null;
444        }
445
446        if ($base64) {
447            return base64_encode($imageBinary);
448        }
449
450        return bin2hex($imageBinary);
451    }
452
453    /**
454     * Check memory image, supported type, image functions, and proportional width/height.
455     */
456    private function checkImage(): void
457    {
458        $this->setSourceType();
459
460        // Check image data
461        if ($this->sourceType == self::SOURCE_ARCHIVE) {
462            $imageData = $this->getArchiveImageSize($this->source);
463        } elseif ($this->sourceType == self::SOURCE_STRING) {
464            $imageData = @getimagesizefromstring($this->source);
465        } else {
466            $imageData = @getimagesize($this->source);
467        }
468        if (!is_array($imageData)) {
469            throw new InvalidImageException(sprintf('Invalid image: %s', $this->source));
470        }
471        [$actualWidth, $actualHeight, $imageType] = $imageData;
472
473        // Check image type support
474        $supportedTypes = [IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG];
475        if ($this->sourceType != self::SOURCE_GD && $this->sourceType != self::SOURCE_STRING) {
476            $supportedTypes = array_merge($supportedTypes, [IMAGETYPE_BMP, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM]);
477        }
478        if (!in_array($imageType, $supportedTypes)) {
479            throw new UnsupportedImageTypeException();
480        }
481
482        // Define image functions
483        $this->imageType = image_type_to_mime_type($imageType);
484        $this->setFunctions();
485        $this->setProportionalSize($actualWidth, $actualHeight);
486    }
487
488    /**
489     * Set source type.
490     */
491    private function setSourceType(): void
492    {
493        if (stripos(strrev($this->source), strrev('.php')) === 0) {
494            $this->memoryImage = true;
495            $this->sourceType = self::SOURCE_GD;
496        } elseif (strpos($this->source, 'zip://') !== false) {
497            $this->memoryImage = false;
498            $this->sourceType = self::SOURCE_ARCHIVE;
499        } elseif (filter_var($this->source, FILTER_VALIDATE_URL) !== false) {
500            $this->memoryImage = true;
501            if (strpos($this->source, 'https') === 0) {
502                $fileContent = file_get_contents($this->source);
503                $this->source = $fileContent;
504                $this->sourceType = self::SOURCE_STRING;
505            } else {
506                $this->sourceType = self::SOURCE_GD;
507            }
508        } elseif ((strpos($this->source, chr(0)) === false) && @file_exists($this->source)) {
509            $this->memoryImage = false;
510            $this->sourceType = self::SOURCE_LOCAL;
511        } else {
512            $this->memoryImage = true;
513            $this->sourceType = self::SOURCE_STRING;
514        }
515    }
516
517    /**
518     * Get image size from archive.
519     *
520     * @since 0.12.0 Throws CreateTemporaryFileException.
521     *
522     * @param string $source
523     *
524     * @return null|array
525     */
526    private function getArchiveImageSize($source)
527    {
528        $imageData = null;
529        $source = substr($source, 6);
530        [$zipFilename, $imageFilename] = explode('#', $source);
531
532        $tempFilename = tempnam(Settings::getTempDir(), 'PHPWordImage');
533        if (false === $tempFilename) {
534            throw new CreateTemporaryFileException(); // @codeCoverageIgnore
535        }
536
537        $zip = new ZipArchive();
538        if ($zip->open($zipFilename) !== false) {
539            if ($zip->locateName($imageFilename) !== false) {
540                $imageContent = $zip->getFromName($imageFilename);
541                if ($imageContent !== false) {
542                    file_put_contents($tempFilename, $imageContent);
543                    $imageData = getimagesize($tempFilename);
544                    unlink($tempFilename);
545                }
546            }
547            $zip->close();
548        }
549
550        return $imageData;
551    }
552
553    /**
554     * Set image functions and extensions.
555     */
556    private function setFunctions(): void
557    {
558        switch ($this->imageType) {
559            case 'image/png':
560                $this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefrompng';
561                $this->imageFunc = function ($resource): void {
562                    imagepng($resource, null, $this->imageQuality);
563                };
564                $this->imageExtension = 'png';
565                $this->imageQuality = -1;
566
567                break;
568            case 'image/gif':
569                $this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefromgif';
570                $this->imageFunc = function ($resource): void {
571                    imagegif($resource);
572                };
573                $this->imageExtension = 'gif';
574                $this->imageQuality = null;
575
576                break;
577            case 'image/jpeg':
578            case 'image/jpg':
579                $this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefromjpeg';
580                $this->imageFunc = function ($resource): void {
581                    imagejpeg($resource, null, $this->imageQuality);
582                };
583                $this->imageExtension = 'jpg';
584                $this->imageQuality = 100;
585
586                break;
587            case 'image/bmp':
588            case 'image/x-ms-bmp':
589                $this->imageType = 'image/bmp';
590                $this->imageFunc = null;
591                $this->imageExtension = 'bmp';
592                $this->imageQuality = null;
593
594                break;
595            case 'image/tiff':
596                $this->imageType = 'image/tiff';
597                $this->imageFunc = null;
598                $this->imageExtension = 'tif';
599                $this->imageQuality = null;
600
601                break;
602        }
603    }
604
605    /**
606     * Set proportional width/height if one dimension not available.
607     *
608     * @param int $actualWidth
609     * @param int $actualHeight
610     */
611    private function setProportionalSize($actualWidth, $actualHeight): void
612    {
613        $styleWidth = $this->style->getWidth();
614        $styleHeight = $this->style->getHeight();
615        if (!($styleWidth && $styleHeight)) {
616            if ($styleWidth == null && $styleHeight == null) {
617                $this->style->setWidth($actualWidth);
618                $this->style->setHeight($actualHeight);
619            } elseif ($styleWidth) {
620                $this->style->setHeight($actualHeight * ($styleWidth / $actualWidth));
621            } else {
622                $this->style->setWidth($actualWidth * ($styleHeight / $actualHeight));
623            }
624        }
625    }
626}