• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 9778188016

03 Jul 2024 12:50PM UTC coverage: 83.729%. First build
9778188016

Pull #2001

github

web-flow
Merge 16ad72c88 into 2aef2094d
Pull Request #2001: refactor: asset image functions

54 of 75 new or added lines in 2 files covered. (72.0%)

2959 of 3534 relevant lines covered (83.73%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.25
/src/Assets/Image.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <arnaud@ligny.fr>
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\ImageManagerStatic as ImageManager;
18

19
class Image
20
{
21
    /**
22
     * Resize an image Asset.
23
     *
24
     * @throws RuntimeException
25
     */
26
    public static function resize(Asset $asset, int $width, int $quality): string
27
    {
28
        try {
29
            // is image Asset?
30
            if ($asset['type'] !== 'image') {
1✔
NEW
31
                throw new RuntimeException(sprintf('Not an image.'));
×
32
            }
33
            // is GD is installed
34
            if (!\extension_loaded('gd')) {
1✔
NEW
35
                throw new RuntimeException('GD extension is required.');
×
36
            }
37
            // creates image object from source
38
            $image = ImageManager::make($asset['content_source']);
1✔
39
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
40
            $image->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
1✔
41
                $constraint->aspectRatio();
1✔
42
                $constraint->upsize();
1✔
43
            });
1✔
44
            // interlaces (PNG) or progressives (JPEG) image
45
            $image->interlace();
1✔
46
            // save image in extension format and given quality
47
            $imageAsString = (string) $image->encode($asset['ext'], $quality);
1✔
48
            // destroy image object
49
            $image->destroy();
1✔
50

51
            return $imageAsString;
1✔
NEW
52
        } catch (\Exception $e) {
×
NEW
53
            throw new RuntimeException(sprintf('Not able to resize "%s": %s', $asset['path'], $e->getMessage()));
×
54
        }
55
    }
56

57
    /**
58
     * Converts an image Asset to the target format.
59
     *
60
     * @throws RuntimeException
61
     */
62
    public static function convert(Asset $asset, string $format, int $quality): string
63
    {
64
        try {
65
            if ($asset['type'] !== 'image') {
1✔
NEW
66
                throw new RuntimeException(sprintf('Not an image.'));
×
67
            }
68
            $image = ImageManager::make($asset['content']);
1✔
69
            $imageAsString = (string) $image->encode($format, $quality);
1✔
70
            $image->destroy();
1✔
71

72
            return $imageAsString;
1✔
NEW
73
        } catch (\Exception $e) {
×
NEW
74
            throw new RuntimeException(sprintf('Not able to resize "%s": %s', $asset['path'], $e->getMessage()));
×
75
        }
76
    }
77

78
    /**
79
     * Returns the Data URL (encoded in Base64).
80
     *
81
     * @throws RuntimeException
82
     */
83
    public static function getDataUrl(Asset $asset, int $quality): string
84
    {
85
        try {
86
            if ($asset['type'] != 'image' || self::isSVG($asset)) {
1✔
NEW
87
                throw new RuntimeException(sprintf('Not an image.'));
×
88
            }
89
            $image = ImageManager::make($asset['content']);
1✔
90
            $imageAsDataUrl = (string) $image->encode('data-url', $quality);
1✔
91
            $image->destroy();
1✔
92

93
            return $imageAsDataUrl;
1✔
NEW
94
        } catch (\Exception $e) {
×
NEW
95
            throw new RuntimeException(sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
×
96
        }
97
    }
98

99
    /**
100
     * Returns the dominant hexadecimal color of an image asset.
101
     *
102
     * @throws RuntimeException
103
     */
104
    public static function getDominantColor(Asset $asset): string
105
    {
106
        try {
107
            if ($asset['type'] != 'image' || self::isSVG($asset)) {
1✔
NEW
108
                throw new RuntimeException(sprintf('Not an image.'));
×
109
            }
110

111
            $assetColor = clone $asset;
1✔
112
            $assetColor = $assetColor->resize(100);
1✔
113
            $image = ImageManager::make($assetColor['content']);
1✔
114
            $color = $image->limitColors(1)->pickColor(0, 0, 'hex');
1✔
115
            $image->destroy();
1✔
116

117
            return $color;
1✔
NEW
118
        } catch (\Exception $e) {
×
NEW
119
            throw new RuntimeException(sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
×
120
        }
121
    }
122

123
    /**
124
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
125
     *
126
     * @throws RuntimeException
127
     */
128
    public static function getLqip(Asset $asset): string
129
    {
130
        try {
131
            if ($asset['type'] !== 'image') {
1✔
NEW
132
                throw new RuntimeException(sprintf('Not an image.'));
×
133
            }
134
            $assetLqip = clone $asset;
1✔
135
            $assetLqip = $assetLqip->resize(100);
1✔
136
            $image = ImageManager::make($assetLqip['content']);
1✔
137
            $imageAsString = (string) $image->blur(50)->encode('data-url');
1✔
138
            $image->destroy();
1✔
139

140
            return $imageAsString;
1✔
NEW
141
        } catch (\Exception $e) {
×
NEW
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
    public static function buildSrcset(Asset $asset, array $widths): string
153
    {
154
        if ($asset['type'] !== 'image') {
1✔
155
            throw new RuntimeException(sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
×
156
        }
157

158
        $srcset = '';
1✔
159
        $widthMax = 0;
1✔
160
        foreach ($widths as $width) {
1✔
161
            if ($asset['width'] < $width) {
1✔
162
                break;
1✔
163
            }
164
            $img = $asset->resize($width);
1✔
165
            $srcset .= sprintf('%s %sw, ', (string) $img, $width);
1✔
166
            $widthMax = $width;
1✔
167
        }
168
        // adds source image
169
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
1✔
170
            $srcset .= sprintf('%s %sw', (string) $asset, $asset['width']);
1✔
171
        }
172

173
        return rtrim($srcset, ', ');
1✔
174
    }
175

176
    /**
177
     * Returns the value of the "sizes" attribute corresponding to the configured class.
178
     */
179
    public static function getSizes(string $class, array $sizes = []): string
180
    {
181
        $result = '';
1✔
182
        $classArray = explode(' ', $class);
1✔
183
        foreach ($classArray as $class) {
1✔
184
            if (\array_key_exists($class, $sizes)) {
1✔
185
                $result = $sizes[$class] . ', ';
1✔
186
            }
187
        }
188
        if (!empty($result)) {
1✔
189
            return trim($result, ', ');
1✔
190
        }
191

192
        return $sizes['default'] ?? '100vw';
1✔
193
    }
194

195
    /**
196
     * Checks if an asset is an animated GIF.
197
     */
198
    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
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content_source']);
1✔
205

206
        return $count > 1;
1✔
207
    }
208

209
    /**
210
     * Returns true if asset is a SVG.
211
     */
212
    public static function isSVG(Asset $asset): bool
213
    {
214
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
1✔
215
    }
216

217
    /**
218
     * Returns SVG attributes.
219
     *
220
     * @return \SimpleXMLElement|false
221
     */
222
    public static function getSvgAttributes(Asset $asset)
223
    {
224
        if (!self::isSVG($asset)) {
1✔
NEW
225
            return false;
×
226
        }
227

228
        if (false === $xml = simplexml_load_string($asset['content_source'])) {
1✔
NEW
229
            return false;
×
230
        }
231

232
        return $xml->attributes();
1✔
233
    }
234
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc