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