Passed
Push — ImageManager ( 9c9f33 )
by Arnaud
05:05
created

Image   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 75
c 2
b 1
f 0
dl 0
loc 207
rs 9.28
wmc 39

11 Methods

Rating   Name   Duplication   Size   Complexity  
A resize() 0 15 3
A isAnimatedGif() 0 9 1
A getSizes() 0 14 4
A manager() 0 10 5
A getDominantColor() 0 13 4
A convert() 0 11 3
A isSVG() 0 3 2
A getSvgAttributes() 0 11 3
A getLqip() 0 13 3
A getDataUrl() 0 11 4
B buildSrcset() 0 22 7
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
    private static function manager(): ImageManager
26
    {
27
        if (\extension_loaded('gd') && \function_exists('gd_info')) {
28
            return ImageManager::gd();
29
        }
30
        if (\extension_loaded('imagick') && class_exists('Imagick')) {
31
            return ImageManager::imagick();
32
        }
33
34
        throw new RuntimeException('PHP GD extension is required.');
35
    }
36
37
    /**
38
     * Resize an image Asset.
39
     *
40
     * @throws RuntimeException
41
     */
42
    public static function resize(Asset $asset, int $width, int $quality): string
43
    {
44
        try {
45
            // is image Asset?
46
            if ($asset['type'] !== 'image') {
47
                throw new RuntimeException(sprintf('Not an image.'));
48
            }
49
            // creates image object from source
50
            $image = self::manager()->read($asset['content_source']);
51
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
52
            $image->scaleDown(width: $width);
53
            // return image data
54
            return (string)$image->encodeByMediaType($asset['subtype'], progressive: true, interlaced: true, quality: $quality);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type Intervention\Image\MediaType|null|string expected by parameter $type of Intervention\Image\Inter...ce::encodeByMediaType(). ( Ignorable by Annotation )

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

54
            return (string)$image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, interlaced: true, quality: $quality);
Loading history...
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
    public static function convert(Asset $asset, string $format, int $quality): string
66
    {
67
        try {
68
            if ($asset['type'] !== 'image') {
69
                throw new RuntimeException(sprintf('Not an image.'));
70
            }
71
            $image = self::manager()->read($asset['content']);
72
73
            return (string)$image->encodeByExtension($format, progressive: true, interlaced: true, quality: $quality);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type Intervention\Image\FileExtension|null|string expected by parameter $extension of Intervention\Image\Inter...ce::encodeByExtension(). ( Ignorable by Annotation )

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

73
            return (string)$image->encodeByExtension($format, /** @scrutinizer ignore-type */ progressive: true, interlaced: true, quality: $quality);
Loading history...
74
        } catch (\Exception $e) {
75
            throw new RuntimeException(sprintf('Not able to convert "%s": %s', $asset['path'], $e->getMessage()));
76
        }
77
    }
78
79
    /**
80
     * Returns the Data URL (encoded in Base64).
81
     *
82
     * @throws RuntimeException
83
     */
84
    public static function getDataUrl(Asset $asset, int $quality): string
85
    {
86
        try {
87
            if ($asset['type'] != 'image' || self::isSVG($asset)) {
88
                throw new RuntimeException(sprintf('Not an image.'));
89
            }
90
            $image = self::manager()->read($asset['content']);
91
92
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
93
        } catch (\Exception $e) {
94
            throw new RuntimeException(sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
95
        }
96
    }
97
98
    /**
99
     * Returns the dominant hexadecimal color of an image asset.
100
     *
101
     * @throws RuntimeException
102
     */
103
    public static function getDominantColor(Asset $asset): string
104
    {
105
        try {
106
            if ($asset['type'] != 'image' || self::isSVG($asset)) {
107
                throw new RuntimeException(sprintf('Not an image.'));
108
            }
109
            $assetColor = clone $asset;
110
            $assetColor = $assetColor->resize(100);
111
            $image = self::manager()->read($assetColor['content']);
112
113
            return $image->reduceColors(1)->pickColor(0, 0)->toHex();
114
        } catch (\Exception $e) {
115
            throw new RuntimeException(sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
116
        }
117
    }
118
119
    /**
120
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
121
     *
122
     * @throws RuntimeException
123
     */
124
    public static function getLqip(Asset $asset): string
125
    {
126
        try {
127
            if ($asset['type'] !== 'image') {
128
                throw new RuntimeException(sprintf('Not an image.'));
129
            }
130
            $assetLqip = clone $asset;
131
            $assetLqip = $assetLqip->resize(100);
132
            $image = self::manager()->read($assetLqip['content']);
133
134
            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
    public static function buildSrcset(Asset $asset, array $widths): string
147
    {
148
        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
        $srcset = '';
153
        $widthMax = 0;
154
        foreach ($widths as $width) {
155
            if ($asset['width'] < $width) {
156
                break;
157
            }
158
            $img = $asset->resize($width);
159
            $srcset .= sprintf('%s %sw, ', (string) $img, $width);
160
            $widthMax = $width;
161
        }
162
        // adds source image
163
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
164
            $srcset .= sprintf('%s %sw', (string) $asset, $asset['width']);
165
        }
166
167
        return rtrim($srcset, ', ');
168
    }
169
170
    /**
171
     * Returns the value of the "sizes" attribute corresponding to the configured class.
172
     */
173
    public static function getSizes(string $class, array $sizes = []): string
174
    {
175
        $result = '';
176
        $classArray = explode(' ', $class);
177
        foreach ($classArray as $class) {
178
            if (\array_key_exists($class, $sizes)) {
179
                $result = $sizes[$class] . ', ';
180
            }
181
        }
182
        if (!empty($result)) {
183
            return trim($result, ', ');
184
        }
185
186
        return $sizes['default'] ?? '100vw';
187
    }
188
189
    /**
190
     * Checks if an asset is an animated GIF.
191
     */
192
    public static function isAnimatedGif(Asset $asset): bool
193
    {
194
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
195
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
196
        // 2. 4 variable bytes
197
        // 3. a static 2-byte sequence (\x00\x2C)
198
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content_source']);
199
200
        return $count > 1;
201
    }
202
203
    /**
204
     * Returns true if asset is a SVG.
205
     */
206
    public static function isSVG(Asset $asset): bool
207
    {
208
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
209
    }
210
211
    /**
212
     * Returns SVG attributes.
213
     *
214
     * @return \SimpleXMLElement|false
215
     */
216
    public static function getSvgAttributes(Asset $asset)
217
    {
218
        if (!self::isSVG($asset)) {
219
            return false;
220
        }
221
222
        if (false === $xml = simplexml_load_string($asset['content_source'] ?? '')) {
223
            return false;
224
        }
225
226
        return $xml->attributes();
227
    }
228
}
229