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

Cecilapp / Cecil / 13786562850

11 Mar 2025 11:08AM UTC coverage: 82.938% (-0.5%) from 83.394%
13786562850

push

github

web-flow
Merge pull request #2133 from Cecilapp/cache

feat: better cache and Twig cache fragments

106 of 124 new or added lines in 11 files covered. (85.48%)

23 existing lines in 6 files now uncovered.

2970 of 3581 relevant lines covered (82.94%)

0.83 hits per line

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

60.61
/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
            // creates image object from source
46
            $image = self::manager()->read($asset['content']);
1✔
47
            // resizes to $width with constraint the aspect-ratio and unwanted upsizing
48
            $image->scaleDown(width: $width);
1✔
49
            // return image data
50
            return (string) $image->encodeByMediaType($asset['subtype'], /** @scrutinizer ignore-type */ progressive: true, /** @scrutinizer ignore-type */ interlaced: false, quality: $quality);
1✔
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
    public static function convert(Asset $asset, string $format, int $quality): string
62
    {
63
        try {
64
            $image = self::manager()->read($asset['content']);
1✔
65

66
            if (!\function_exists("image$format")) {
1✔
67
                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);
1✔
71
        } catch (\Exception $e) {
×
72
            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
    public static function getDataUrl(Asset $asset, int $quality): string
82
    {
83
        try {
84
            $image = self::manager()->read($asset['content']);
1✔
85

86
            return (string) $image->encode(new AutoEncoder(quality: $quality))->toDataUri();
1✔
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
    public static function getDominantColor(Asset $asset): string
98
    {
99
        try {
100
            $assetColor = clone $asset;
1✔
101
            $assetColor = $assetColor->resize(100);
1✔
102
            $image = self::manager()->read($assetColor['content']);
1✔
103

104
            return $image->reduceColors(1)->pickColor(0, 0)->toString();
1✔
105
        } catch (\Exception $e) {
×
106
            throw new RuntimeException(\sprintf('Can\'t get dominant color of "%s": %s', $asset['path'], $e->getMessage()));
×
107
        }
108
    }
109

110
    /**
111
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
112
     *
113
     * @throws RuntimeException
114
     */
115
    public static function getLqip(Asset $asset): string
116
    {
117
        try {
118
            $assetLqip = clone $asset;
1✔
119
            $assetLqip = $assetLqip->resize(100);
1✔
120
            $image = self::manager()->read($assetLqip['content']);
1✔
121

122
            return (string) $image->blur(50)->encode()->toDataUri();
1✔
123
        } catch (\Exception $e) {
×
124
            throw new RuntimeException(\sprintf('can\'t create LQIP of "%s": %s', $asset['path'], $e->getMessage()));
×
125
        }
126
    }
127

128
    /**
129
     * Build the `srcset` attribute for responsive images.
130
     * e.g.: `srcset="/img-480.jpg 480w, /img-800.jpg 800w"`.
131
     *
132
     * @throws RuntimeException
133
     */
134
    public static function buildSrcset(Asset $asset, array $widths): string
135
    {
136
        if ($asset['type'] !== 'image') {
1✔
137
            throw new RuntimeException(\sprintf('can\'t build "srcset" of "%s": it\'s not an image file.', $asset['path']));
×
138
        }
139

140
        $srcset = '';
1✔
141
        $widthMax = 0;
1✔
142
        sort($widths, SORT_NUMERIC);
1✔
143
        $widths = array_reverse($widths);
1✔
144
        foreach ($widths as $width) {
1✔
145
            if ($asset['width'] < $width) {
1✔
146
                break;
1✔
147
            }
UNCOV
148
            $img = $asset->resize($width);
×
UNCOV
149
            $srcset .= \sprintf('%s %sw, ', (string) $img, $width);
×
UNCOV
150
            $widthMax = $width;
×
151
        }
152
        // adds source image
153
        if (!empty($srcset) && ($asset['width'] < max($widths) && $asset['width'] != $widthMax)) {
1✔
UNCOV
154
            $srcset .= \sprintf('%s %sw', (string) $asset, $asset['width']);
×
155
        }
156

157
        return rtrim($srcset, ', ');
1✔
158
    }
159

160
    /**
161
     * Returns the value of the "sizes" attribute corresponding to the configured class.
162
     */
163
    public static function getSizes(string $class, array $sizes = []): string
164
    {
165
        $result = '';
1✔
166
        $classArray = explode(' ', $class);
1✔
167
        foreach ($classArray as $class) {
1✔
168
            if (\array_key_exists($class, $sizes)) {
1✔
UNCOV
169
                $result = $sizes[$class] . ', ';
×
170
            }
171
        }
172
        if (!empty($result)) {
1✔
UNCOV
173
            return trim($result, ', ');
×
174
        }
175

176
        return $sizes['default'] ?? '100vw';
1✔
177
    }
178

179
    /**
180
     * Checks if an asset is an animated GIF.
181
     */
182
    public static function isAnimatedGif(Asset $asset): bool
183
    {
184
        // an animated GIF contains multiple "frames", with each frame having a header made up of:
185
        // 1. a static 4-byte sequence (\x00\x21\xF9\x04)
186
        // 2. 4 variable bytes
187
        // 3. a static 2-byte sequence (\x00\x2C)
188
        $count = preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', (string) $asset['content']);
1✔
189

190
        return $count > 1;
1✔
191
    }
192

193
    /**
194
     * Returns true if asset is a SVG.
195
     */
196
    public static function isSVG(Asset $asset): bool
197
    {
198
        return \in_array($asset['subtype'], ['image/svg', 'image/svg+xml']) || $asset['ext'] == 'svg';
1✔
199
    }
200

201
    /**
202
     * Asset is a valid image?
203
     */
204
    public static function isImage(Asset $asset): bool
205
    {
206
        if ($asset['type'] !== 'image' || self::isSVG($asset)) {
×
207
            return false;
×
208
        }
209

210
        return true;
×
211
    }
212

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

224
        if (false === $xml = simplexml_load_string($asset['content'] ?? '')) {
1✔
225
            return false;
×
226
        }
227

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