Passed
Pull Request — master (#2143)
by Arnaud
11:37 queued 05:44
created

Image::isImage()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 0
cts 4
cp 0
crap 12
rs 10
c 1
b 0
f 0
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
        // GD first because it's the faster driver
32 1
        if (\extension_loaded('gd') && \function_exists('gd_info')) {
33 1
            $driver = GdDriver::class;
34
        }
35
        // fallback to ImageMagick
36 1
        if (\extension_loaded('imagick') && class_exists('Imagick')) {
37
            $driver = ImagickDriver::class;
38
        }
39
40 1
        if ($driver) {
41 1
            return ImageManager::withDriver(
42 1
                $driver,
43 1
                autoOrientation: true,
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type Intervention\Image\Inter...\DriverInterface|string expected by parameter $driver of Intervention\Image\ImageManager::withDriver(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

43
                /** @scrutinizer ignore-type */ autoOrientation: true,
Loading history...
44 1
                decodeAnimation: true,
45 1
                blendingColor: 'ffffff',
46 1
                strip: true // remove metadata
47 1
            );
48
        }
49
50
        throw new RuntimeException('PHP GD or Imagick extension is required.');
51
    }
52
53
    /**
54
     * Resize an image Asset.
55
     *
56
     * @throws RuntimeException
57
     */
58 1
    public static function resize(Asset $asset, int $width, int $quality): string
59
    {
60
        try {
61
            // creates image object from source
62 1
            $image = self::manager()->read($asset['content']);
63
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
64 1
            $image->scaleDown(width: $width);
65
            // return image data
66 1
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
67
        } catch (\Exception $e) {
68
            throw new RuntimeException(\sprintf('Asset "%s" can\'t be resized: %s', $asset['path'], $e->getMessage()));
69
        }
70
    }
71
72
    /**
73
     * Converts an image Asset to the target format.
74
     *
75
     * @throws RuntimeException
76
     */
77 1
    public static function convert(Asset $asset, string $format, int $quality): string
78
    {
79
        try {
80 1
            $image = self::manager()->read($asset['content']);
81
82 1
            if (!\function_exists("image$format")) {
83 1
                throw new RuntimeException(\sprintf('Function "image%s" is not available.', $format));
84
            }
85
86
            return (string) $image->encodeByExtension($format, /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
87 1
        } catch (\Exception $e) {
88 1
            throw new RuntimeException(\sprintf('Not able to convert "%s": %s', $asset['path'], $e->getMessage()));
89
        }
90
    }
91
92
    /**
93
     * Returns the Data URL (encoded in Base64).
94
     *
95
     * @throws RuntimeException
96
     */
97 1
    public static function getDataUrl(Asset $asset, int $quality): string
98
    {
99
        try {
100 1
            $image = self::manager()->read($asset['content']);
101
102 1
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
103
        } catch (\Exception $e) {
104
            throw new RuntimeException(\sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
105
        }
106
    }
107
108
    /**
109
     * Returns the dominant RGB color of an image asset.
110
     *
111
     * @throws RuntimeException
112
     */
113 1
    public static function getDominantColor(Asset $asset): string
114
    {
115
        try {
116 1
            $image = self::manager()->read(self::resize($asset, 100, 50));
117
118 1
            return $image->reduceColors(1)->pickColor(0, 0)->toString();
119
        } catch (\Exception $e) {
120
            throw new RuntimeException(\sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
121
        }
122
    }
123
124
    /**
125
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
126
     *
127
     * @throws RuntimeException
128
     */
129 1
    public static function getLqip(Asset $asset): string
130
    {
131
        try {
132 1
            $image = self::manager()->read(self::resize($asset, 100, 50));
133
134 1
            return (string) $image->blur(50)->encode()->toDataUri();
135
        } catch (\Exception $e) {
136
            throw new RuntimeException(\sprintf('can\'t create LQIP of "%s": %s', $asset['path'], $e->getMessage()));
137
        }
138
    }
139
140
    /**
141
     * Build the `srcset` attribute for responsive images.
142
     * e.g.: `srcset="/img-480.jpg 480w, /img-800.jpg 800w"`.
143
     *
144
     * @throws RuntimeException
145
     */
146 1
    public static function buildSrcset(Asset $asset, array $widths): string
147
    {
148 1
        if ($asset['type'] !== 'image') {
149
            throw new RuntimeException(\sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
150
        }
151
152 1
        $srcset = '';
153 1
        $widthMax = 0;
154 1
        sort($widths, SORT_NUMERIC);
155 1
        $widths = array_reverse($widths);
156 1
        foreach ($widths as $width) {
157 1
            if ($asset['width'] < $width) {
158 1
                break;
159
            }
160
            $img = $asset->resize($width);
161
            $srcset .= \sprintf('%s %sw, ', (string) $img, $width);
162
            $widthMax = $width;
163
        }
164
        // adds source image
165 1
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
166
            $srcset .= \sprintf('%s %sw', (string) $asset, $asset['width']);
167
        }
168
169 1
        return rtrim($srcset, ', ');
170
    }
171
172
    /**
173
     * Returns the value of the "sizes" attribute corresponding to the configured class.
174
     */
175 1
    public static function getSizes(string $class, array $sizes = []): string
176
    {
177 1
        $result = '';
178 1
        $classArray = explode(' ', $class);
179 1
        foreach ($classArray as $class) {
180 1
            if (\array_key_exists($class, $sizes)) {
181
                $result = $sizes[$class] . ', ';
182
            }
183
        }
184 1
        if (!empty($result)) {
185
            return trim($result, ', ');
186
        }
187
188 1
        return $sizes['default'] ?? '100vw';
189
    }
190
191
    /**
192
     * Checks if an asset is an animated GIF.
193
     */
194 1
    public static function isAnimatedGif(Asset $asset): bool
195
    {
196
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
197
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
198
        // 2. 4 variable bytes
199
        // 3. a static 2-byte sequence (\x00\x2C)
200 1
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content']);
201
202 1
        return $count > 1;
203
    }
204
205
    /**
206
     * Returns true if asset is a SVG.
207
     */
208 1
    public static function isSVG(Asset $asset): bool
209
    {
210 1
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
211
    }
212
213
    /**
214
     * Asset is a valid image?
215
     */
216
    public static function isImage(Asset $asset): bool
217
    {
218
        if ($asset['type'] !== 'image' || self::isSVG($asset)) {
219
            return false;
220
        }
221
222
        return true;
223
    }
224
225
    /**
226
     * Returns SVG attributes.
227
     *
228
     * @return \SimpleXMLElement|false
229
     */
230 1
    public static function getSvgAttributes(Asset $asset)
231
    {
232 1
        if (!self::isSVG($asset)) {
233
            return false;
234
        }
235
236 1
        if (false === $xml = simplexml_load_string($asset['content'] ?? '')) {
237
            return false;
238
        }
239
240 1
        return $xml->attributes();
241
    }
242
}
243