Passed
Push — master ( f509b3...5b74be )
by Eric
12:34 queued 10:26
created

Image::isWebp()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Utility - Collection of various PHP utility functions.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @version   2.0.0
10
 * @copyright (C) 2017 - 2024 Eric Sizemore
11
 * @license   The MIT License (MIT)
12
 *
13
 * Copyright (C) 2017 - 2024 Eric Sizemore <https://www.secondversion.com>.
14
 *
15
 * Permission is hereby granted, free of charge, to any person obtaining a copy
16
 * of this software and associated documentation files (the "Software"), to
17
 * deal in the Software without restriction, including without limitation the
18
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
19
 * sell copies of the Software, and to permit persons to whom the Software is
20
 * furnished to do so, subject to the following conditions:
21
 *
22
 * The above copyright notice and this permission notice shall be included in
23
 * all copies or substantial portions of the Software.
24
 *
25
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31
 * THE SOFTWARE.
32
 */
33
34
namespace Esi\Utility;
35
36
// Exceptions
37
use InvalidArgumentException;
38
use RuntimeException;
39
40
// Functions
41
use function extension_loaded;
42
use function class_exists;
43
use function is_int;
44
use function image_type_to_mime_type;
45
use function explode;
46
use function getimagesize;
47
use function in_array;
48
49
/**
50
 * Image utilities.
51
 *
52
 * @since 2.0.0
53
 */
54
final class Image
55
{
56
    /**
57
     * Image type/mime strings to determine image type.
58
     *
59
     * @var array<string, array<string>> IMAGE_TYPES
60
     */
61
    public const IMAGE_TYPES = [
62
        'jpg'  => ['image/jpg', 'image/jpeg'],
63
        'gif'  => ['image/gif'],
64
        'png'  => ['image/png'],
65
        'webp' => ['image/webp'],
66
    ];
67
68
    /**
69
     * Check if the GD library is available on the server.
70
     *
71
     * @return bool
72
     */
73
    public static function isGdAvailable(): bool
74
    {
75
        //@codeCoverageIgnoreStart
76
        static $hasGd;
77
78
        $hasGd ??= extension_loaded('gd');
79
80
        return $hasGd;
81
        //@codeCoverageIgnoreEnd
82
    }
83
84
    /**
85
     * Check if the GraphicsMagick library is available on the server.
86
     *
87
     * @return bool
88
     */
89
    public static function isGmagickAvailable(): bool
90
    {
91
        //@codeCoverageIgnoreStart
92
        static $hasGmagick;
93
94
        $hasGmagick ??= extension_loaded('gmagick');
95
96
        return $hasGmagick;
97
        //@codeCoverageIgnoreEnd
98
    }
99
100
    /**
101
     * Check if the ImageMagick library is available on the server.
102
     *
103
     * @return bool
104
     */
105
    public static function isImagickAvailable(): bool
106
    {
107
        //@codeCoverageIgnoreStart
108
        static $hasImagick;
109
110
        $hasImagick ??= extension_loaded('imagick');
111
112
        return $hasImagick;
113
        //@codeCoverageIgnoreEnd
114
    }
115
116
    /**
117
     * Check if the Exif extension is available on the server.
118
     *
119
     * @return bool
120
     */
121
    public static function isExifAvailable(): bool
122
    {
123
        //@codeCoverageIgnoreStart
124
        static $hasExif;
125
126
        $hasExif ??= extension_loaded('exif');
127
128
        return $hasExif;
129
        //@codeCoverageIgnoreEnd
130
    }
131
132
    /**
133
     * Helper function for guessImageType().
134
     *
135
     * If the Exif extension is available, use Exif to determine mime type.
136
     *
137
     * @param   string        $imagePath  File path to the image.
138
     * @return  string|false              Returns the image type string on success, false on any failure.
139
     */
140
    private static function guessImageTypeExif(string $imagePath): string | false
141
    {
142
        //@codeCoverageIgnoreStart
143
        // Ignoring code coverage as if one method is available over another, the others won't be or need to be tested
144
        $imageType = @\exif_imagetype($imagePath);
145
146
        return (is_int($imageType) ? image_type_to_mime_type($imageType) : false);
147
        //@codeCoverageIgnoreEnd
148
    }
149
150
    /**
151
     * Helper function for guessImageType().
152
     *
153
     * If the FileInfo (finfo) extension is available, use finfo to determine mime type.
154
     *
155
     * @param   string        $imagePath  File path to the image.
156
     * @return  string|false              Returns the image type string on success, false on any failure.
157
     */
158
    private static function guessImageTypeFinfo(string $imagePath): string | false
159
    {
160
        //@codeCoverageIgnoreStart
161
        // Ignoring code coverage as if one method is available over another, the others won't be or need to be tested
162
        $finfo = new \finfo(\FILEINFO_MIME);
163
        $result = $finfo->file($imagePath);
164
165
        if ($result === false) {
166
            // false means an error occured
167
            return false;
168
        }
169
170
        [$mime, ] = explode('; ', $result);
171
172
        if (Strings::beginsWith($mime, 'image/')) {
173
            return $mime;
174
        }
175
        return false;
176
        //@codeCoverageIgnoreEnd
177
    }
178
179
    /**
180
     * Helper function for guessImageType()
181
     *
182
     * If the Exif extension is available, use Exif to determine mime type.
183
     *
184
     * @param   string        $imagePath  File path to the image.
185
     * @return  string|false              Returns the image type string on success, false on any failure.
186
     */
187
    private static function guessImageTypeGetImageSize(string $imagePath): string | false
188
    {
189
        //@codeCoverageIgnoreStart
190
        // Ignoring code coverage as if one method is available over another, the others won't be or need to be tested
191
        $imageSize = @getimagesize($imagePath);
192
193
        return $imageSize['mime'] ?? false;
194
        //@codeCoverageIgnoreEnd
195
    }
196
197
    /**
198
     * Attempts to determine the image type. It tries to determine the image type with, in order
199
     * of preference: Exif, finfo, and getimagesize.
200
     *
201
     * @param   string        $imagePath  File path to the image.
202
     * @return  string|false              Returns the image type string on success, false on any failure.
203
     */
204 10
    public static function guessImageType(string $imagePath): string | false
205
    {
206 10
        static $hasFinfo;
207
208 10
        $hasFinfo ??= class_exists('finfo');
209
210 10
        if (!Filesystem::isFile($imagePath)) {
211 1
            throw new InvalidArgumentException('$imagePath not found or is not a file.');
212
        }
213
214
        // If Exif is available, let's start there. It's the fastest.
215
        //@codeCoverageIgnoreStart
216
        if (self::isExifAvailable()) {
217
            return self::guessImageTypeExif($imagePath);
218
        }
219
220
        if ($hasFinfo) {
221
            // Next, let's try finfo
222
            return self::guessImageTypeFinfo($imagePath);
223
        }
224
225
        // Last resort: getimagesize can be pretty slow, especially compared to exif_imagetype
226
        // It may not return "mime". Theoretically, this should only happen for a file that is not an image.
227
        return self::guessImageTypeGetImageSize($imagePath);
228
        //@codeCoverageIgnoreEnd
229
    }
230
231
    /**
232
     * Checks if image has JPG format.
233
     *
234
     * @param   string  $imagePath  File path to the image.
235
     * @return  bool
236
     *
237
     * @throws InvalidArgumentException If the image path provided is not valid.
238
     * @throws RuntimeException         If we are unable to determine the file type.
239
     */
240 3
    public static function isJpg(string $imagePath): bool
241
    {
242 3
        if (!Filesystem::isFile($imagePath)) {
243 1
            throw new InvalidArgumentException('$imagePath not found or is not a file.');
244
        }
245
246 2
        $imageType = self::guessImageType($imagePath);
247
248 2
        if ($imageType === false) {
249 1
            throw new RuntimeException('Unable to determine the image type. Is it a valid image file?');
250
        }
251 1
        return in_array($imageType, self::IMAGE_TYPES['jpg'], true);
252
    }
253
254
    /**
255
     * Checks if image has GIF format.
256
     *
257
     * @param   string  $imagePath  File path to the image.
258
     * @return  bool
259
     *
260
     * @throws InvalidArgumentException If the image path provided is not valid.
261
     * @throws RuntimeException         If we are unable to determine the file type.
262
     */
263 3
    public static function isGif(string $imagePath): bool
264
    {
265 3
        if (!Filesystem::isFile($imagePath)) {
266 1
            throw new InvalidArgumentException('$imagePath not found or is not a file.');
267
        }
268
269 2
        $imageType = self::guessImageType($imagePath);
270
271 2
        if ($imageType === false) {
272 1
            throw new RuntimeException('Unable to determine the image type. Is it a valid image file?');
273
        }
274 1
        return in_array($imageType, self::IMAGE_TYPES['gif'], true);
275
    }
276
277
    /**
278
     * Checks if image has PNG format.
279
     *
280
     * @param   string  $imagePath  File path to the image.
281
     * @return  bool
282
     *
283
     * @throws InvalidArgumentException If the image path provided is not valid.
284
     * @throws RuntimeException         If we are unable to determine the file type.
285
     */
286 3
    public static function isPng(string $imagePath): bool
287
    {
288 3
        if (!Filesystem::isFile($imagePath)) {
289 1
            throw new InvalidArgumentException('$imagePath not found or is not a file.');
290
        }
291
292 2
        $imageType = self::guessImageType($imagePath);
293
294 2
        if ($imageType === false) {
295 1
            throw new RuntimeException('Unable to determine the image type. Is it a valid image file?');
296
        }
297 1
        return in_array($imageType, self::IMAGE_TYPES['png'], true);
298
    }
299
300
    /**
301
     * Checks if image has WEBP format.
302
     *
303
     * @param   string  $imagePath  File path to the image.
304
     * @return  bool
305
     *
306
     * @throws InvalidArgumentException If the image path provided is not valid.
307
     * @throws RuntimeException         If we are unable to determine the file type.
308
     */
309 3
    public static function isWebp(string $imagePath): bool
310
    {
311 3
        if (!Filesystem::isFile($imagePath)) {
312 1
            throw new InvalidArgumentException('$imagePath not found or is not a file.');
313
        }
314
315 2
        $imageType = self::guessImageType($imagePath);
316
317 2
        if ($imageType === false) {
318 1
            throw new RuntimeException('Unable to determine the image type. Is it a valid image file?');
319
        }
320 1
        return in_array($imageType, self::IMAGE_TYPES['webp'], true);
321
    }
322
}
323