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

Cecilapp / Cecil / 13950208528

19 Mar 2025 03:19PM UTC coverage: 82.77%. First build
13950208528

Pull #2142

github

web-flow
Merge 7673cf772 into 322b79b33
Pull Request #2142: feat: add support of libvips

4 of 11 new or added lines in 1 file covered. (36.36%)

2964 of 3581 relevant lines covered (82.77%)

0.83 hits per line

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

56.72
/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\Vips\Driver as VipsDriver;
18
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
19
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
20
use Intervention\Image\Encoders\AutoEncoder;
21
use Intervention\Image\ImageManager;
22

23
class Image
24
{
25
    /**
26
     * Create new manager instance with available driver, faster first.
27
     */
28
    private static function manager(): ImageManager
29
    {
30
        try {
31
            // Vips is the fastest driver
32
            return ImageManager::withDriver(VipsDriver::class);
1✔
33
        } catch (\Exception) {
1✔
34
            // fallback to GD or Imagick
35
            if (\extension_loaded('gd') && \function_exists('gd_info')) {
1✔
36
                return ImageManager::withDriver(GdDriver::class);
1✔
37
            }
NEW
38
            if (\extension_loaded('imagick') && class_exists('Imagick')) {
×
NEW
39
                return ImageManager::withDriver(ImagickDriver::class);
×
40
            }
41
        }
42

43
        throw new RuntimeException('PHP GD or Imagick extension is required.');
×
44
    }
45

46
    /**
47
     * Resize an image Asset.
48
     *
49
     * @throws RuntimeException
50
     */
51
    public static function resize(Asset $asset, int $width, int $quality): string
52
    {
53
        try {
54
            // creates image object from source
55
            $image = self::manager()->read($asset['content']);
1✔
56
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
57
            $image->scaleDown(width: $width);
1✔
58
            // return image data
59
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
60
        } catch (\Exception $e) {
×
61
            throw new RuntimeException(\sprintf('Asset "%s" can\'t be resized: %s', $asset['path'], $e->getMessage()));
×
62
        }
63
    }
64

65
    /**
66
     * Converts an image Asset to the target format.
67
     *
68
     * @throws RuntimeException
69
     */
70
    public static function convert(Asset $asset, string $format, int $quality): string
71
    {
72
        try {
73
            $image = self::manager()->read($asset['content']);
1✔
74

75
            if (!\function_exists("image$format")) {
1✔
76
                throw new RuntimeException(\sprintf('Function "image%s" is not available.', $format));
×
77
            }
78

79
            return (string) $image->encodeByExtension($format, /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
80
        } catch (\Exception $e) {
×
81
            throw new RuntimeException(\sprintf('Not able to convert "%s": %s', $asset['path'], $e->getMessage()));
×
82
        }
83
    }
84

85
    /**
86
     * Returns the Data URL (encoded in Base64).
87
     *
88
     * @throws RuntimeException
89
     */
90
    public static function getDataUrl(Asset $asset, int $quality): string
91
    {
92
        try {
93
            $image = self::manager()->read($asset['content']);
1✔
94

95
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
1✔
96
        } catch (\Exception $e) {
×
97
            throw new RuntimeException(\sprintf('Can\'t get Data URL of "%s": %s', $asset['path'], $e->getMessage()));
×
98
        }
99
    }
100

101
    /**
102
     * Returns the dominant RGB color of an image asset.
103
     *
104
     * @throws RuntimeException
105
     */
106
    public static function getDominantColor(Asset $asset): string
107
    {
108
        try {
109
            $image = self::manager()->read(self::resize($asset, 100, 50));
1✔
110

111
            return $image->reduceColors(1)->pickColor(0, 0)->toString();
1✔
NEW
112
        } catch (\Exception) {
×
113
            // fallback to GD driver
114
            try {
NEW
115
                $image = ImageManager::withDriver(GdDriver::class)->read(self::resize($asset, 100, 50));
×
116

NEW
117
                return $image->reduceColors(1)->pickColor(0, 0)->toString();
×
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
    /**
125
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
126
     *
127
     * @throws RuntimeException
128
     */
129
    public static function getLqip(Asset $asset): string
130
    {
131
        try {
132
            $image = self::manager()->read(self::resize($asset, 100, 50));
1✔
133

134
            return (string) $image->blur(50)->encode()->toDataUri();
1✔
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') {
1✔
149
            throw new RuntimeException(\sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
×
150
        }
151

152
        $srcset = '';
1✔
153
        $widthMax = 0;
1✔
154
        sort($widths, SORT_NUMERIC);
1✔
155
        $widths = array_reverse($widths);
1✔
156
        foreach ($widths as $width) {
1✔
157
            if ($asset['width'] < $width) {
1✔
158
                break;
1✔
159
            }
160
            $img = $asset->resize($width);
×
161
            $srcset .= \sprintf('%s %sw, ', (string) $img, $width);
×
162
            $widthMax = $width;
×
163
        }
164
        // adds source image
165
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
1✔
166
            $srcset .= \sprintf('%s %sw', (string) $asset, $asset['width']);
×
167
        }
168

169
        return rtrim($srcset, ', ');
1✔
170
    }
171

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

188
        return $sizes['default'] ?? '100vw';
1✔
189
    }
190

191
    /**
192
     * Checks if an asset is an animated GIF.
193
     */
194
    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
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content']);
1✔
201

202
        return $count > 1;
1✔
203
    }
204

205
    /**
206
     * Returns true if asset is a SVG.
207
     */
208
    public static function isSVG(Asset $asset): bool
209
    {
210
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
1✔
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
    public static function getSvgAttributes(Asset $asset)
231
    {
232
        if (!self::isSVG($asset)) {
1✔
233
            return false;
×
234
        }
235

236
        if (false === $xml = simplexml_load_string($asset['content'] ?? '')) {
1✔
237
            return false;
×
238
        }
239

240
        return $xml->attributes();
1✔
241
    }
242
}
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