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

Cecilapp / Cecil / 10085634239

24 Jul 2024 11:47PM UTC coverage: 81.305%. First build
10085634239

Pull #2017

github

web-flow
Merge e78bf6975 into a8f2266d1
Pull Request #2017: feat: multiple image formats support (webp, avif)

6 of 72 new or added lines in 4 files covered. (8.33%)

2879 of 3541 relevant lines covered (81.3%)

0.81 hits per line

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

60.56
/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\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')) {
1✔
28
            return ImageManager::gd();
1✔
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
    public static function resize(Asset $asset, int $width, int $quality): string
43
    {
44
        try {
45
            // is image Asset?
46
            if ($asset['type'] !== 'image') {
1✔
47
                throw new RuntimeException(sprintf('Not an image.'));
×
48
            }
49
            // creates image object from source
50
            $image = self::manager()->read($asset['content_source']);
1✔
51
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
52
            $image->scaleDown(width: $width);
1✔
53
            // return image data
54
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
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
            if (!\function_exists("image$format")) {
×
74
                throw new RuntimeException(sprintf('Function "image%s" is not available.', $format));
×
75
            }
76

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

83
    /**
84
     * Returns the Data URL (encoded in Base64).
85
     *
86
     * @throws RuntimeException
87
     */
88
    public static function getDataUrl(Asset $asset, int $quality): string
89
    {
90
        try {
91
            if ($asset['type'] != 'image' || self::isSVG($asset)) {
1✔
92
                throw new RuntimeException(sprintf('Not an image.'));
×
93
            }
94
            $image = self::manager()->read($asset['content']);
1✔
95

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

102
    /**
103
     * Returns the dominant RGB color of an image asset.
104
     *
105
     * @throws RuntimeException
106
     */
107
    public static function getDominantColor(Asset $asset): string
108
    {
109
        try {
110
            if ($asset['type'] != 'image' || self::isSVG($asset)) {
1✔
111
                throw new RuntimeException(sprintf('Not an image.'));
×
112
            }
113
            $assetColor = clone $asset;
1✔
114
            $assetColor = $assetColor->resize(100);
1✔
115
            $image = self::manager()->read($assetColor['content']);
1✔
116

117
            return $image->pickColor(0, 0)->toString();
1✔
118
        } catch (\Exception $e) {
×
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✔
132
                throw new RuntimeException(sprintf('Not an image.'));
×
133
            }
134
            $assetLqip = clone $asset;
1✔
135
            $assetLqip = $assetLqip->resize(100);
1✔
136
            $image = self::manager()->read($assetLqip['content']);
1✔
137

138
            return (string) $image->blur(50)->encode()->toDataUri();
1✔
139
        } catch (\Exception $e) {
×
140
            throw new RuntimeException(sprintf('can\'t create LQIP of "%s": %s', $asset['path'], $e->getMessage()));
×
141
        }
142
    }
143

144
    /**
145
     * Build the `srcset` attribute for responsive images.
146
     * e.g.: `srcset="/img-480.jpg 480w, /img-800.jpg 800w"`.
147
     *
148
     * @throws RuntimeException
149
     */
150
    public static function buildSrcset(Asset $asset, array $widths): string
151
    {
152
        if ($asset['type'] !== 'image') {
1✔
153
            throw new RuntimeException(sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
×
154
        }
155

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

171
        return rtrim($srcset, ', ');
1✔
172
    }
173

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

190
        return $sizes['default'] ?? '100vw';
1✔
191
    }
192

193
    /**
194
     * Checks if an asset is an animated GIF.
195
     */
196
    public static function isAnimatedGif(Asset $asset): bool
197
    {
198
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
199
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
200
        // 2. 4 variable bytes
201
        // 3. a static 2-byte sequence (\x00\x2C)
202
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content_source']);
×
203

204
        return $count > 1;
×
205
    }
206

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

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

226
        if (false === $xml = simplexml_load_string($asset['content_source'] ?? '')) {
1✔
227
            return false;
×
228
        }
229

230
        return $xml->attributes();
1✔
231
    }
232
}
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