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

Cecilapp / Cecil / 15440770441

04 Jun 2025 11:11AM UTC coverage: 82.775% (-0.2%) from 82.968%
15440770441

Pull #2172

github

web-flow
Merge c88048483 into 33dbed85a
Pull Request #2172: Build Command : Allow to render a subset

23 of 39 new or added lines in 3 files covered. (58.97%)

173 existing lines in 4 files now uncovered.

3114 of 3762 relevant lines covered (82.78%)

0.83 hits per line

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

79.75
/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\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
    private static function manager(): ImageManager
28
    {
29
        $driver = null;
1✔
30

31
        // ImageMagick is available? (for a future quality option)
32
        if (\extension_loaded('imagick') && class_exists('Imagick')) {
1✔
33
            $driver = ImagickDriver::class;
1✔
34
        }
35
        // Use GD, because it's the faster driver
36
        if (\extension_loaded('gd') && \function_exists('gd_info')) {
1✔
37
            $driver = GdDriver::class;
1✔
38
        }
39

40
        if ($driver) {
1✔
41
            return ImageManager::withDriver(
1✔
42
                $driver,
1✔
43
                [
1✔
44
                    'autoOrientation' => true,
1✔
45
                    'decodeAnimation' => true,
1✔
46
                    'blendingColor' => 'ffffff',
1✔
47
                    'strip' => true, // remove metadata
1✔
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
    public static function resize(Asset $asset, int $width, int $quality): string
61
    {
62
        try {
63
            // creates image object from source
64
            $image = self::manager()->read($asset['content']);
1✔
65
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
66
            $image->scaleDown(width: $width);
1✔
67
            // return image data
68
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
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
    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
            $image = self::manager()->read($asset['content']);
1✔
84
            // crops the image
85
            $image->cover(width: $width, height: $height, position: $position);
1✔
86
            // return image data
87
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
UNCOV
88
        } catch (\Exception $e) {
×
89
            throw new RuntimeException(\sprintf('Asset "%s" can\'t be cropped: %s', $asset['path'], $e->getMessage()));
×
90
        }
91
    }
92

93
    /**
94
     * Converts an image Asset to the target format.
95
     *
96
     * @throws RuntimeException
97
     */
98
    public static function convert(Asset $asset, string $format, int $quality): string
99
    {
100
        try {
101
            $image = self::manager()->read($asset['content']);
1✔
102

103
            if (!\function_exists("image$format")) {
1✔
UNCOV
104
                throw new RuntimeException(\sprintf('Function "image%s" is not available.', $format));
×
105
            }
106

107
            return (string) $image->encodeByExtension($format, /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
UNCOV
108
        } catch (\Exception $e) {
×
UNCOV
109
            throw new RuntimeException(\sprintf('Not able to convert "%s" to %s: %s', $asset['path'], $format, $e->getMessage()));
×
110
        }
111
    }
112

113
    /**
114
     * Returns the Data URL (encoded in Base64).
115
     *
116
     * @throws RuntimeException
117
     */
118
    public static function getDataUrl(Asset $asset, int $quality): string
119
    {
120
        try {
121
            $image = self::manager()->read($asset['content']);
1✔
122

123
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
1✔
UNCOV
124
        } catch (\Exception $e) {
×
UNCOV
125
            throw new RuntimeException(\sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
×
126
        }
127
    }
128

129
    /**
130
     * Returns the dominant RGB color of an image asset.
131
     *
132
     * @throws RuntimeException
133
     */
134
    public static function getDominantColor(Asset $asset): string
135
    {
136
        try {
137
            $image = self::manager()->read(self::resize($asset, 100, 50));
1✔
138

139
            return $image->reduceColors(1)->pickColor(0, 0)->toString();
1✔
UNCOV
140
        } catch (\Exception $e) {
×
UNCOV
141
            throw new RuntimeException(\sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
×
142
        }
143
    }
144

145
    /**
146
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
147
     *
148
     * @throws RuntimeException
149
     */
150
    public static function getLqip(Asset $asset): string
151
    {
152
        try {
153
            $image = self::manager()->read(self::resize($asset, 100, 50));
1✔
154

155
            return (string) $image->blur(50)->encode()->toDataUri();
1✔
UNCOV
156
        } catch (\Exception $e) {
×
UNCOV
157
            throw new RuntimeException(\sprintf('can\'t create LQIP of "%s": %s', $asset['path'], $e->getMessage()));
×
158
        }
159
    }
160

161
    /**
162
     * Build the `srcset` attribute for responsive images.
163
     * e.g.: `srcset="/img-480.jpg 480w, /img-800.jpg 800w"`.
164
     *
165
     * @throws RuntimeException
166
     */
167
    public static function buildSrcset(Asset $asset, array $widths): string
168
    {
169
        if (!self::isImage($asset)) {
1✔
170
            throw new RuntimeException(\sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
1✔
171
        }
172

173
        $srcset = '';
1✔
174
        $widthMax = 0;
1✔
175
        sort($widths, SORT_NUMERIC);
1✔
176
        $widths = array_reverse($widths);
1✔
177
        foreach ($widths as $width) {
1✔
178
            if ($asset['width'] < $width) {
1✔
179
                continue;
1✔
180
            }
181
            $img = $asset->resize($width);
1✔
182
            $srcset .= \sprintf('%s %sw, ', (string) $img, $width);
1✔
183
            $widthMax = $width;
1✔
184
        }
185
        // adds source image
186
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
1✔
187
            $srcset .= \sprintf('%s %sw', (string) $asset, $asset['width']);
1✔
188
        }
189

190
        return rtrim($srcset, ', ');
1✔
191
    }
192

193
    /**
194
     * Returns the value of the "sizes" attribute corresponding to the configured class.
195
     */
196
    public static function getSizes(string $class, array $sizes = []): string
197
    {
198
        $result = '';
1✔
199
        $classArray = explode(' ', $class);
1✔
200
        foreach ($classArray as $class) {
1✔
201
            if (\array_key_exists($class, $sizes)) {
1✔
202
                $result = $sizes[$class] . ', ';
1✔
203
            }
204
        }
205
        if (!empty($result)) {
1✔
206
            return trim($result, ', ');
1✔
207
        }
208

209
        return $sizes['default'] ?? '100vw';
1✔
210
    }
211

212
    /**
213
     * Checks if an asset is an animated GIF.
214
     */
215
    public static function isAnimatedGif(Asset $asset): bool
216
    {
217
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
218
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
219
        // 2. 4 variable bytes
220
        // 3. a static 2-byte sequence (\x00\x2C)
221
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content']);
1✔
222

223
        return $count > 1;
1✔
224
    }
225

226
    /**
227
     * Returns true if asset is a SVG.
228
     */
229
    public static function isSVG(Asset $asset): bool
230
    {
231
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
1✔
232
    }
233

234
    /**
235
     * Returns true if asset is an ICO.
236
     */
237
    public static function isIco(Asset $asset): bool
238
    {
239
        return \in_array($asset['subtype'], ['image/x-icon', 'image/vnd.microsoft.icon']) || $asset['ext'] == 'ico';
1✔
240
    }
241

242
    /**
243
     * Asset is a valid image?
244
     */
245
    public static function isImage(Asset $asset): bool
246
    {
247
        if ($asset['type'] !== 'image' || self::isSVG($asset) || self::isIco($asset)) {
1✔
248
            return false;
1✔
249
        }
250

251
        return true;
1✔
252
    }
253

254
    /**
255
     * Returns SVG attributes.
256
     *
257
     * @return \SimpleXMLElement|false
258
     */
259
    public static function getSvgAttributes(Asset $asset)
260
    {
261
        if (!self::isSVG($asset)) {
1✔
UNCOV
262
            return false;
×
263
        }
264

265
        if (false === $xml = simplexml_load_string($asset['content'] ?? '')) {
1✔
UNCOV
266
            return false;
×
267
        }
268

269
        return $xml->attributes();
1✔
270
    }
271
}
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