-
Notifications
You must be signed in to change notification settings - Fork 641
/
Copy pathImages.php
454 lines (385 loc) · 13.2 KB
/
Images.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\services;
use Craft;
use craft\base\Image;
use craft\helpers\App;
use craft\helpers\ConfigHelper;
use craft\helpers\FileHelper;
use craft\helpers\Image as ImageHelper;
use craft\image\Raster;
use craft\image\Svg;
use craft\image\SvgAllowedAttributes;
use enshrined\svgSanitize\Sanitizer;
use Imagine\Gd\Imagine as GdImagine;
use Imagine\Image\Format;
use Imagine\Imagick\Imagick;
use Imagine\Imagick\Imagine as ImagickImagine;
use Throwable;
use yii\base\Component;
use yii\base\Exception;
/**
* Images service.
*
* An instance of the service is available via [[\craft\base\ApplicationTrait::getImages()|`Craft::$app->images`]].
*
* @property bool $isGd Whether image manipulations will be performed using GD or not
* @property bool $isImagick Whether image manipulations will be performed using Imagick or not
* @property array $supportedImageFormats A list of all supported image formats
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 3.0.0
*/
class Images extends Component
{
public const DRIVER_GD = 'gd';
public const DRIVER_IMAGICK = 'imagick';
public const MINIMUM_IMAGICK_VERSION = '6.2.9';
/**
* @var array Image formats that can be manipulated.
*/
public array $supportedImageFormats = ['jpg', 'jpeg', 'gif', 'png'];
/**
* @var string Image driver.
*/
private string $_driver = '';
/**
* @var string|null Imagick version being used, if any.
*/
private ?string $_imagickVersion = null;
/**
* Decide on the image driver being used.
*/
public function init(): void
{
if (strtolower(Craft::$app->getConfig()->getGeneral()->imageDriver) === 'gd') {
$this->_driver = self::DRIVER_GD;
} elseif ($this->getCanUseImagick()) {
$this->_driver = self::DRIVER_IMAGICK;
} else {
$this->_driver = self::DRIVER_GD;
}
parent::init();
}
/**
* Returns whether image manipulations will be performed using GD or not.
*
* @return bool
*/
public function getIsGd(): bool
{
return $this->_driver === self::DRIVER_GD;
}
/**
* Returns whether image manipulations will be performed using Imagick or not.
*
* @return bool
*/
public function getIsImagick(): bool
{
return $this->_driver === self::DRIVER_IMAGICK;
}
/**
* Returns the version of the image driver.
*
* @return string
*/
public function getVersion(): string
{
if ($this->getIsGd()) {
return App::extensionVersion('gd');
}
$version = App::extensionVersion('imagick');
try {
$version .= ' (ImageMagick ' . $this->getImageMagickApiVersion() . ')';
} catch (Throwable) {
}
return $version;
}
/**
* Returns a list of all supported image formats.
*
* @return array
*/
public function getSupportedImageFormats(): array
{
$supportedFormats = $this->supportedImageFormats;
if ($this->getSupportsWebP()) {
$supportedFormats[] = Format::ID_WEBP;
}
if ($this->getSupportsAvif()) {
$supportedFormats[] = Format::ID_AVIF;
}
if ($this->getSupportsHeic()) {
$supportedFormats[] = Format::ID_HEIC;
}
return $supportedFormats;
}
/**
* Returns the installed ImageMagick API version.
*
* @return string
* @throws Exception if the Imagick extension isn’t installed
*/
public function getImageMagickApiVersion(): string
{
if (isset($this->_imagickVersion)) {
return $this->_imagickVersion;
}
if (!extension_loaded('imagick')) {
throw new Exception('The Imagick extension isn’t loaded.');
}
// Taken from Imagick\Imagine() constructor.
// Imagick::getVersion() is static only since Imagick PECL extension 3.2.0b1, so instantiate it.
$versionString = \Imagick::getVersion()['versionString'];
[$this->_imagickVersion] = sscanf($versionString, 'ImageMagick %s %04d-%02d-%02d %s %s');
return $this->_imagickVersion;
}
/**
* Returns whether Imagick is installed and meets version requirements
*
* @return bool
*/
public function getCanUseImagick(): bool
{
if (!extension_loaded('imagick')) {
return false;
}
// https://github.com/craftcms/cms/issues/5435
if (empty(\Imagick::queryFormats())) {
return false;
}
// Make sure it meets the minimum API version requirement
if (version_compare($this->getImageMagickApiVersion(), self::MINIMUM_IMAGICK_VERSION) === -1) {
return false;
}
return true;
}
/**
* Returns whether the WebP image format is supported.
*
* @return bool
*/
public function getSupportsWebP(): bool
{
$info = $this->getIsImagick() ? ImagickImagine::getDriverInfo() : GdImagine::getDriverInfo();
return $info->isFormatSupported(Format::ID_WEBP);
}
/**
* Returns whether the AVIF image format is supported.
*
* @return bool
*/
public function getSupportsAvif(): bool
{
$info = $this->getIsImagick() ? ImagickImagine::getDriverInfo() : GdImagine::getDriverInfo();
return $info->isFormatSupported(Format::ID_AVIF);
}
/**
* Returns whether the HEIC/HEIF image format is supported.
*
* @return bool
* @since 4.3.6
*/
public function getSupportsHeic(): bool
{
$info = $this->getIsImagick() ? ImagickImagine::getDriverInfo() : GdImagine::getDriverInfo();
return $info->isFormatSupported(Format::ID_HEIC);
}
/**
* Loads an image from a file system path.
*
* @param string $path
* @param bool $rasterize Whether the image should be rasterized if it's an SVG
* @param int $svgSize The size SVG should be scaled up to, if rasterized
* @return Image
*/
public function loadImage(string $path, bool $rasterize = false, int $svgSize = 1000): Image
{
if (FileHelper::isSvg($path)) {
$image = new Svg();
$image->loadImage($path);
if ($rasterize) {
$image->scaleToFit($svgSize, $svgSize);
$svgString = $image->getSvgString();
$image = new Raster();
$image->loadFromSVG($svgString);
}
} else {
$image = new Raster();
$image->loadImage($path);
}
return $image;
}
/**
* Determines if there is enough memory to process this image.
*
* The code was adapted from http://www.php.net/manual/en/function.imagecreatefromjpeg.php#64155.
* It will first attempt to do it with available memory. If that fails,
* Craft will bump the memory to amount defined by the
* <config5:phpMaxMemoryLimit> config setting, then try again.
*
* @param string $filePath The path to the image file.
* @param bool $toTheMax If set to true, will set the PHP memory to the config setting phpMaxMemoryLimit.
* @return bool
*/
public function checkMemoryForImage(string $filePath, bool $toTheMax = false): bool
{
if (FileHelper::isSvg($filePath)) {
return true;
}
if (!function_exists('memory_get_usage')) {
return false;
}
if ($toTheMax) {
// Turn it up to 11.
App::maxPowerCaptain();
}
// If the file is 0bytes, we probably have enough memory
if (!filesize($filePath)) {
return true;
}
// Find out how much memory this image is going to need.
$imageInfo = getimagesize($filePath);
// If we can't find out the imagesize, chances are, we won't be able to anything about it.
if (!is_array($imageInfo)) {
Craft::warning('Could not determine image information for ' . $filePath);
return true;
}
$K64 = 65536;
$tweakFactor = 1.7;
$bits = $imageInfo['bits'] ?? 8;
$channels = $imageInfo['channels'] ?? 4;
$memoryNeeded = round(($imageInfo[0] * $imageInfo[1] * $bits * $channels / 8 + $K64) * $tweakFactor);
$memoryLimit = ConfigHelper::sizeInBytes(ini_get('memory_limit'));
if ($memoryLimit == -1 || memory_get_usage() + $memoryNeeded < $memoryLimit) {
return true;
}
if (!$toTheMax) {
return $this->checkMemoryForImage($filePath, true);
}
// Oh well, we tried.
return false;
}
/**
* Cleans an image by its path, clearing embedded potentially malicious embedded code.
*
* @param string $filePath
* @throws Exception if $filePath is a malformed SVG image
*/
public function cleanImage(string $filePath): void
{
$cleanedByRotation = false;
$cleanedByStripping = false;
// Special case for SVG files.
if (FileHelper::isSvg($filePath)) {
if (!Craft::$app->getConfig()->getGeneral()->sanitizeSvgUploads) {
return;
}
$sanitizer = new Sanitizer();
$sanitizer->setAllowedAttrs(new SvgAllowedAttributes());
$svgContents = file_get_contents($filePath);
$svgContents = $sanitizer->sanitize($svgContents);
if (!$svgContents) {
throw new Exception('There was a problem sanitizing the SVG file contents, likely due to malformed XML.');
}
file_put_contents($filePath, $svgContents);
return;
}
if (FileHelper::isGif($filePath) && !Craft::$app->getConfig()->getGeneral()->transformGifs) {
return;
}
try {
if (Craft::$app->getConfig()->getGeneral()->rotateImagesOnUploadByExifData) {
$cleanedByRotation = $this->rotateImageByExifData($filePath);
}
$cleanedByStripping = $this->stripOrientationFromExifData($filePath);
} catch (Throwable $e) {
Craft::error('Tried to rotate or strip EXIF data from image and failed: ' . $e->getMessage(), __METHOD__);
}
// Image has already been cleaned if it had exif/orientation data
if ($cleanedByRotation || $cleanedByStripping) {
return;
}
$this->loadImage($filePath)->saveAs($filePath, true);
}
/**
* Rotate image according to it's EXIF data.
*
* @param string $filePath
* @return bool
*/
public function rotateImageByExifData(string $filePath): bool
{
if (!ImageHelper::canHaveExifData($filePath)) {
return false;
}
// Quick and dirty, if possible
if (!($this->getIsImagick() && method_exists(\Imagick::class, 'getImageOrientation'))) {
return false;
}
$image = new \Imagick($filePath);
$orientation = $image->getImageOrientation();
$degrees = match ($orientation) {
ImageHelper::EXIF_IFD0_ROTATE_180, ImageHelper::EXIF_IFD0_ROTATE_180_MIRRORED => 180,
ImageHelper::EXIF_IFD0_ROTATE_90, ImageHelper::EXIF_IFD0_ROTATE_90_MIRRORED => 90,
ImageHelper::EXIF_IFD0_ROTATE_270, ImageHelper::EXIF_IFD0_ROTATE_270_MIRRORED => 270,
default => 0,
};
$mirrored = match ($orientation) {
ImageHelper::EXIF_IFD0_ROTATE_0_MIRRORED, ImageHelper::EXIF_IFD0_ROTATE_180_MIRRORED,
ImageHelper::EXIF_IFD0_ROTATE_90_MIRRORED, ImageHelper::EXIF_IFD0_ROTATE_270_MIRRORED => true,
default => false,
};
if ($degrees === 0 && !$mirrored) {
return false;
}
/** @var Raster $image */
$image = $this->loadImage($filePath);
if ($degrees !== 0) {
$image->rotate($degrees);
}
if ($mirrored) {
$image->flipHorizontally();
}
return $image->saveAs($filePath, true);
}
/**
* Get EXIF metadata for a file by it's path.
*
* @param string $filePath
* @return array|null
*/
public function getExifData(string $filePath): ?array
{
if (!ImageHelper::canHaveExifData($filePath)) {
return null;
}
$image = new Raster();
return $image->getExifMetadata($filePath);
}
/**
* Strip orientation from EXIF data for an image at a path.
*
* @param string $filePath
* @return bool
*/
public function stripOrientationFromExifData(string $filePath): bool
{
if (!ImageHelper::canHaveExifData($filePath)) {
return false;
}
// Quick and dirty, if possible
if (!($this->getIsImagick() && method_exists(\Imagick::class, 'setImageOrientation'))) {
return false;
}
$image = new \Imagick($filePath);
$image->setImageOrientation(\Imagick::ORIENTATION_UNDEFINED);
ImageHelper::cleanExifDataFromImagickImage($image);
$image->writeImages($filePath, true);
return true;
}
}