Passed
Push — master ( 71e118...2de46f )
by Arnaud
05:56
created

Image::getDataUrl()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

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