Passed
Push — master ( 465ad2...870d7b )
by Arnaud
06:10
created

Image::isImage()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
ccs 3
cts 4
cp 0.75
crap 3.1406
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Exception\RuntimeException;
17
use Intervention\Image\Encoders\AutoEncoder;
18
use Intervention\Image\ImageManager;
19
20
class Image
21
{
22
    /**
23
     * Create new manager instance with desired driver.
24
     */
25 1
    private static function manager(): ImageManager
26
    {
27 1
        if (\extension_loaded('gd') && \function_exists('gd_info')) {
28 1
            return ImageManager::gd();
29
        }
30
        if (\extension_loaded('imagick') && class_exists('Imagick')) {
31
            return ImageManager::imagick();
32
        }
33
34
        throw new RuntimeException('PHP GD or Imagick extension is required.');
35
    }
36
37
    /**
38
     * Resize an image Asset.
39
     *
40
     * @throws RuntimeException
41
     */
42 1
    public static function resize(Asset $asset, int $width, int $quality): string
43
    {
44
        try {
45
            // is image?
46 1
            if (!self::isImage($asset)) {
47
                throw new RuntimeException(\sprintf('Not an image.'));
48
            }
49
            // creates image object from source
50 1
            $image = self::manager()->read($asset['content_source']);
51
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
52 1
            $image->scaleDown(width: $width);
53
            // return image data
54 1
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
55
        } catch (\Exception $e) {
56
            throw new RuntimeException(\sprintf('Not able to resize "%s": %s', $asset['path'], $e->getMessage()));
57
        }
58
    }
59
60
    /**
61
     * Converts an image Asset to the target format.
62
     *
63
     * @throws RuntimeException
64
     */
65 1
    public static function convert(Asset $asset, string $format, int $quality): string
66
    {
67
        try {
68 1
            if ($asset['type'] !== 'image') {
69
                throw new RuntimeException(\sprintf('Not an image.'));
70
            }
71 1
            $image = self::manager()->read($asset['content']);
72
73 1
            if (!\function_exists("image$format")) {
74 1
                throw new RuntimeException(\sprintf('Function "image%s" is not available.', $format));
75
            }
76
77
            return (string) $image->encodeByExtension($format, /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
78 1
        } catch (\Exception $e) {
79 1
            throw new RuntimeException(\sprintf('Not able to convert "%s": %s', $asset['path'], $e->getMessage()));
80
        }
81
    }
82
83
    /**
84
     * Returns the Data URL (encoded in Base64).
85
     *
86
     * @throws RuntimeException
87
     */
88 1
    public static function getDataUrl(Asset $asset, int $quality): string
89
    {
90
        try {
91
            // is image?
92 1
            if (!self::isImage($asset)) {
93
                throw new RuntimeException(\sprintf('Not an image.'));
94
            }
95 1
            $image = self::manager()->read($asset['content']);
96
97 1
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
98
        } catch (\Exception $e) {
99
            throw new RuntimeException(\sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
100
        }
101
    }
102
103
    /**
104
     * Returns the dominant RGB color of an image asset.
105
     *
106
     * @throws RuntimeException
107
     */
108 1
    public static function getDominantColor(Asset $asset): string
109
    {
110
        try {
111
            // is image?
112 1
            if (!self::isImage($asset)) {
113
                throw new RuntimeException(\sprintf('Not an image.'));
114
            }
115 1
            $assetColor = clone $asset;
116 1
            $assetColor = $assetColor->resize(100);
117 1
            $image = self::manager()->read($assetColor['content']);
118
119 1
            return $image->pickColor(0, 0)->toString();
120
        } catch (\Exception $e) {
121
            throw new RuntimeException(\sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
122
        }
123
    }
124
125
    /**
126
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
127
     *
128
     * @throws RuntimeException
129
     */
130 1
    public static function getLqip(Asset $asset): string
131
    {
132
        try {
133 1
            if ($asset['type'] !== 'image') {
134
                throw new RuntimeException(\sprintf('Not an image.'));
135
            }
136 1
            $assetLqip = clone $asset;
137 1
            $assetLqip = $assetLqip->resize(100);
138 1
            $image = self::manager()->read($assetLqip['content']);
139
140 1
            return (string) $image->blur(50)->encode()->toDataUri();
141
        } catch (\Exception $e) {
142
            throw new RuntimeException(\sprintf('can\'t create LQIP of "%s": %s', $asset['path'], $e->getMessage()));
143
        }
144
    }
145
146
    /**
147
     * Build the `srcset` attribute for responsive images.
148
     * e.g.: `srcset="/img-480.jpg 480w, /img-800.jpg 800w"`.
149
     *
150
     * @throws RuntimeException
151
     */
152 1
    public static function buildSrcset(Asset $asset, array $widths): string
153
    {
154 1
        if ($asset['type'] !== 'image') {
155
            throw new RuntimeException(\sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
156
        }
157
158 1
        $srcset = '';
159 1
        $widthMax = 0;
160 1
        foreach ($widths as $width) {
161 1
            if ($asset['width'] < $width) {
162 1
                break;
163
            }
164 1
            $img = $asset->resize($width);
165 1
            $srcset .= \sprintf('%s %sw, ', (string) $img, $width);
166 1
            $widthMax = $width;
167
        }
168
        // adds source image
169 1
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
170 1
            $srcset .= \sprintf('%s %sw', (string) $asset, $asset['width']);
171
        }
172
173 1
        return rtrim($srcset, ', ');
174
    }
175
176
    /**
177
     * Returns the value of the "sizes" attribute corresponding to the configured class.
178
     */
179 1
    public static function getSizes(string $class, array $sizes = []): string
180
    {
181 1
        $result = '';
182 1
        $classArray = explode(' ', $class);
183 1
        foreach ($classArray as $class) {
184 1
            if (\array_key_exists($class, $sizes)) {
185 1
                $result = $sizes[$class] . ', ';
186
            }
187
        }
188 1
        if (!empty($result)) {
189 1
            return trim($result, ', ');
190
        }
191
192 1
        return $sizes['default'] ?? '100vw';
193
    }
194
195
    /**
196
     * Checks if an asset is an animated GIF.
197
     */
198 1
    public static function isAnimatedGif(Asset $asset): bool
199
    {
200
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
201
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
202
        // 2. 4 variable bytes
203
        // 3. a static 2-byte sequence (\x00\x2C)
204 1
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content_source']);
205
206 1
        return $count > 1;
207
    }
208
209
    /**
210
     * Returns true if asset is a SVG.
211
     */
212 1
    public static function isSVG(Asset $asset): bool
213
    {
214 1
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
215
    }
216
217
    /**
218
     * Asset is a valid image?
219
     */
220 1
    public static function isImage(Asset $asset): bool
221
    {
222 1
        if ($asset['type'] !== 'image' || self::isSVG($asset)) {
223
            return false;
224
        }
225
226 1
        return true;
227
    }
228
229
    /**
230
     * Returns SVG attributes.
231
     *
232
     * @return \SimpleXMLElement|false
233
     */
234 1
    public static function getSvgAttributes(Asset $asset)
235
    {
236 1
        if (!self::isSVG($asset)) {
237
            return false;
238
        }
239
240 1
        if (false === $xml = simplexml_load_string($asset['content_source'] ?? '')) {
241
            return false;
242
        }
243
244 1
        return $xml->attributes();
245
    }
246
}
247