Passed
Push — master ( d819f8...f783d6 )
by
unknown
04:55
created

Image::convert()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 5
nop 3
dl 0
loc 12
ccs 6
cts 7
cp 0.8571
crap 3.0261
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\Drivers\Gd\Driver as GdDriver;
18
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
19
use Intervention\Image\Encoders\AutoEncoder;
20
use Intervention\Image\ImageManager;
21
22
class Image
23
{
24
    /**
25
     * Create new manager instance with available driver.
26
     */
27 1
    private static function manager(): ImageManager
28
    {
29 1
        $driver = null;
30
31
        // ImageMagick is available? (for a future quality option)
32 1
        if (\extension_loaded('imagick') && class_exists('Imagick')) {
33
            $driver = ImagickDriver::class;
34
        }
35
        // Use GD, because it's the faster driver
36 1
        if (\extension_loaded('gd') && \function_exists('gd_info')) {
37 1
            $driver = GdDriver::class;
38
        }
39
40 1
        if ($driver) {
41 1
            return ImageManager::withDriver(
42 1
                $driver,
43 1
                [
44 1
                    'autoOrientation' => true,
45 1
                    'decodeAnimation' => true,
46 1
                    'blendingColor' => 'ffffff',
47 1
                    'strip' => true, // remove metadata
48 1
                ]
49 1
            );
50
        }
51
52
        throw new RuntimeException('PHP GD (or Imagick) extension is required.');
53
    }
54
55
    /**
56
     * Scales down an image Asset to the given width, keeping the aspect ratio.
57
     *
58
     * @throws RuntimeException
59
     */
60 1
    public static function resize(Asset $asset, int $width, int $quality): string
61
    {
62
        try {
63
            // creates image object from source
64 1
            $image = self::manager()->read($asset['content']);
65
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
66 1
            $image->scaleDown(width: $width);
67
            // return image data
68 1
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
69
        } catch (\Exception $e) {
70
            throw new RuntimeException(\sprintf('Asset "%s" can\'t be resized: %s', $asset['path'], $e->getMessage()));
71
        }
72
    }
73
74
    /**
75
     * Crops an image Asset to the given width and height, keeping the aspect ratio.
76
     *
77
     * @throws RuntimeException
78
     */
79 1
    public static function cover(Asset $asset, int $width, int $height, string $position, int $quality): string
80
    {
81
        try {
82
            // creates image object from source
83 1
            $image = self::manager()->read($asset['content']);
84
            // turns an animated image (i.e GIF) into a static image
85 1
            if ($image->isAnimated()) {
86
                $image = $image->removeAnimation();
87
            }
88
            // crops the image
89 1
            $image->cover(width: $width, height: $height, position: $position);
90
            // return image data
91 1
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
92
        } catch (\Exception $e) {
93
            throw new RuntimeException(\sprintf('Asset "%s" can\'t be cropped: %s', $asset['path'], $e->getMessage()));
94
        }
95
    }
96
97
    /**
98
     * Converts an image Asset to the target format.
99
     *
100
     * @throws RuntimeException
101
     */
102 1
    public static function convert(Asset $asset, string $format, int $quality): string
103
    {
104
        try {
105 1
            $image = self::manager()->read($asset['content']);
106
107 1
            if (!\function_exists("image$format")) {
108 1
                throw new RuntimeException(\sprintf('Function "image%s" is not available.', $format));
109
            }
110
111
            return (string) $image->encodeByExtension($format, /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
112 1
        } catch (\Exception $e) {
113 1
            throw new RuntimeException(\sprintf('Not able to convert "%s" to %s: %s', $asset['path'], $format, $e->getMessage()));
114
        }
115
    }
116
117
    /**
118
     * Returns the Data URL (encoded in Base64).
119
     *
120
     * @throws RuntimeException
121
     */
122 1
    public static function getDataUrl(Asset $asset, int $quality): string
123
    {
124
        try {
125 1
            $image = self::manager()->read($asset['content']);
126
127 1
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
128
        } catch (\Exception $e) {
129
            throw new RuntimeException(\sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
130
        }
131
    }
132
133
    /**
134
     * Returns the dominant RGB color of an image asset.
135
     *
136
     * @throws RuntimeException
137
     */
138 1
    public static function getDominantColor(Asset $asset): string
139
    {
140
        try {
141 1
            $image = self::manager()->read(self::resize($asset, 100, 50));
142
143 1
            return $image->reduceColors(1)->pickColor(0, 0)->toString();
144
        } catch (\Exception $e) {
145
            throw new RuntimeException(\sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
146
        }
147
    }
148
149
    /**
150
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
151
     *
152
     * @throws RuntimeException
153
     */
154 1
    public static function getLqip(Asset $asset): string
155
    {
156
        try {
157 1
            $image = self::manager()->read(self::resize($asset, 100, 50));
158
159 1
            return (string) $image->blur(50)->encode()->toDataUri();
160
        } catch (\Exception $e) {
161
            throw new RuntimeException(\sprintf('can\'t create LQIP of "%s": %s', $asset['path'], $e->getMessage()));
162
        }
163
    }
164
165
    /**
166
     * Build the `srcset` attribute for responsive images.
167
     * e.g.: `srcset="/img-480.jpg 480w, /img-800.jpg 800w"`.
168
     *
169
     * @throws RuntimeException
170
     */
171 1
    public static function buildSrcset(Asset $asset, array $widths): string
172
    {
173 1
        if (!self::isImage($asset)) {
174 1
            throw new RuntimeException(\sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
175
        }
176
177 1
        $srcset = '';
178 1
        $widthMax = 0;
179 1
        sort($widths, SORT_NUMERIC);
180 1
        $widths = array_reverse($widths);
181 1
        foreach ($widths as $width) {
182 1
            if ($asset['width'] < $width) {
183 1
                continue;
184
            }
185 1
            $img = $asset->resize($width);
186 1
            $srcset .= \sprintf('%s %sw, ', (string) $img, $width);
187 1
            $widthMax = $width;
188
        }
189
        // adds source image
190 1
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
191 1
            $srcset .= \sprintf('%s %sw', (string) $asset, $asset['width']);
192
        }
193
194 1
        return rtrim($srcset, ', ');
195
    }
196
197
    /**
198
     * Returns the value of the "sizes" attribute corresponding to the configured class.
199
     */
200 1
    public static function getSizes(string $class, array $sizes = []): string
201
    {
202 1
        $result = '';
203 1
        $classArray = explode(' ', $class);
204 1
        foreach ($classArray as $class) {
205 1
            if (\array_key_exists($class, $sizes)) {
206 1
                $result = $sizes[$class] . ', ';
207
            }
208
        }
209 1
        if (!empty($result)) {
210 1
            return trim($result, ', ');
211
        }
212
213 1
        return $sizes['default'] ?? '100vw';
214
    }
215
216
    /**
217
     * Checks if an asset is an animated GIF.
218
     */
219 1
    public static function isAnimatedGif(Asset $asset): bool
220
    {
221
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
222
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
223
        // 2. 4 variable bytes
224
        // 3. a static 2-byte sequence (\x00\x2C)
225 1
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content']);
226
227 1
        return $count > 1;
228
    }
229
230
    /**
231
     * Returns true if asset is a SVG.
232
     */
233 1
    public static function isSVG(Asset $asset): bool
234
    {
235 1
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
236
    }
237
238
    /**
239
     * Returns true if asset is an ICO.
240
     */
241 1
    public static function isIco(Asset $asset): bool
242
    {
243 1
        return \in_array($asset['subtype'], ['image/x-icon', 'image/vnd.microsoft.icon']) || $asset['ext'] == 'ico';
244
    }
245
246
    /**
247
     * Asset is a valid image?
248
     */
249 1
    public static function isImage(Asset $asset): bool
250
    {
251 1
        if ($asset['type'] !== 'image' || self::isSVG($asset) || self::isIco($asset)) {
252 1
            return false;
253
        }
254
255 1
        return true;
256
    }
257
258
    /**
259
     * Returns SVG attributes.
260
     *
261
     * @return \SimpleXMLElement|false
262
     */
263 1
    public static function getSvgAttributes(Asset $asset)
264
    {
265 1
        if (!self::isSVG($asset)) {
266
            return false;
267
        }
268
269 1
        if (false === $xml = simplexml_load_string($asset['content'] ?? '')) {
270
            return false;
271
        }
272
273 1
        return $xml->attributes();
274
    }
275
}
276