Passed
Pull Request — develop (#245)
by
unknown
05:50
created

Optimize::serverSupportsWebP()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 10
rs 10
cc 4
nc 3
nop 0
1
<?php
2
/**
3
 * ImageOptimize plugin for Craft CMS 3.x
4
 *
5
 * Automatically optimize images after they've been transformed
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\imageoptimize\services;
12
13
use nystudio107\imageoptimize\ImageOptimize;
14
use nystudio107\imageoptimize\imagetransforms\CraftImageTransform;
15
use nystudio107\imageoptimize\imagetransforms\ImageTransform;
16
use nystudio107\imageoptimize\imagetransforms\ImageTransformInterface;
17
use nystudio107\imageoptimizeimgix\imagetransforms\ImgixImageTransform;
18
use nystudio107\imageoptimizethumbor\imagetransforms\ThumborImageTransform;
19
use nystudio107\imageoptimizesharp\imagetransforms\SharpImageTransform;
20
21
use Craft;
22
use craft\base\Component;
23
use craft\base\Image;
24
use craft\elements\Asset;
25
use craft\errors\ImageException;
26
use craft\errors\VolumeException;
27
use craft\events\AssetTransformImageEvent;
28
use craft\events\GetAssetThumbUrlEvent;
29
use craft\events\GetAssetUrlEvent;
30
use craft\events\GenerateTransformEvent;
31
use craft\events\RegisterComponentTypesEvent;
32
use craft\helpers\Component as ComponentHelper;
33
use craft\helpers\FileHelper;
34
use craft\helpers\Assets as AssetsHelper;
35
use craft\helpers\Image as ImageHelper;
36
use craft\image\Raster;
37
use craft\models\AssetTransform;
38
use craft\models\AssetTransformIndex;
39
40
use mikehaertl\shellcommand\Command as ShellCommand;
41
42
use yii\base\InvalidConfigException;
43
44
/** @noinspection MissingPropertyAnnotationsInspection */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
45
46
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
47
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
48
 * @package   ImageOptimize
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
49
 * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
50
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
51
class Optimize extends Component
52
{
53
    // Constants
54
    // =========================================================================
55
56
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
57
     * @event RegisterComponentTypesEvent The event that is triggered when registering
58
     *        Image Transform types
59
     *
60
     * Image Transform types must implement [[ImageTransformInterface]]. [[ImageTransform]]
61
     * provides a base implementation.
62
     *
63
     * ```php
64
     * use nystudio107\imageoptimize\services\Optimize;
65
     * use craft\events\RegisterComponentTypesEvent;
66
     * use yii\base\Event;
67
     *
68
     * Event::on(Optimize::class,
69
     *     Optimize::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES,
70
     *     function(RegisterComponentTypesEvent $event) {
71
     *         $event->types[] = MyImageTransform::class;
72
     *     }
73
     * );
74
     * ```
75
     */
76
    const EVENT_REGISTER_IMAGE_TRANSFORM_TYPES = 'registerImageTransformTypes';
77
78
    const DEFAULT_IMAGE_TRANSFORM_TYPES = [
79
        CraftImageTransform::class,
80
        ImgixImageTransform::class,
81
        SharpImageTransform::class,
82
        ThumborImageTransform::class,
83
    ];
84
85
    // Public Methods
86
    // =========================================================================
87
88
    /**
89
     * Returns all available field type classes.
90
     *
91
     * @return string[] The available field type classes
92
     */
93
    public function getAllImageTransformTypes(): array
94
    {
95
        $imageTransformTypes = array_unique(array_merge(
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
96
            ImageOptimize::$plugin->getSettings()->defaultImageTransformTypes ?? [],
97
            self::DEFAULT_IMAGE_TRANSFORM_TYPES
98
        ), SORT_REGULAR);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
99
100
        $event = new RegisterComponentTypesEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
101
            'types' => $imageTransformTypes
102
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
103
        $this->trigger(self::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES, $event);
104
105
        return $event->types;
106
    }
107
108
    /**
109
     * Creates an Image Transform with a given config.
110
     *
111
     * @param mixed $config The Image Transform’s class name, or its config,
112
     *                      with a `type` value and optionally a `settings` value
113
     *
114
     * @return null|ImageTransformInterface The Image Transform
115
     */
116
    public function createImageTransformType($config): ImageTransformInterface
117
    {
118
        if (is_string($config)) {
119
            $config = ['type' => $config];
120
        }
121
122
        try {
123
            /** @var ImageTransform $imageTransform */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
124
            $imageTransform = ComponentHelper::createComponent($config, ImageTransformInterface::class);
125
        } catch (\Throwable $e) {
126
            $imageTransform = null;
127
            Craft::error($e->getMessage(), __METHOD__);
128
        }
129
130
        return $imageTransform;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $imageTransform could return the type null which is incompatible with the type-hinted return nystudio107\imageoptimiz...ImageTransformInterface. Consider adding an additional type-check to rule them out.
Loading history...
131
    }
132
133
    /**
134
     * Handle responding to EVENT_GET_ASSET_URL events
135
     *
136
     * @param GetAssetUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
137
     *
138
     * @return null|string
139
     * @throws InvalidConfigException
140
     */
141
    public function handleGetAssetUrlEvent(GetAssetUrlEvent $event)
142
    {
143
        Craft::beginProfile('handleGetAssetUrlEvent', __METHOD__);
144
        $url = null;
145
        if (!ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform) {
146
            $asset = $event->asset;
147
            $transform = $event->transform;
148
            // If the transform is empty in some regard, normalize it to null
149
            if (empty($transform)) {
150
                $transform = null;
151
            }
152
            // If there's no transform requested, and we can't manipulate the image anyway, just return the URL
153
            if ($transform === null
154
                && !ImageHelper::canManipulateAsImage(pathinfo($asset->filename, PATHINFO_EXTENSION))) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
155
                $volume = $asset->getVolume();
156
157
                return AssetsHelper::generateUrl($volume, $asset);
158
            }
159
            // If we're passed in null, make a dummy AssetTransform model for Thumbor
160
            // For backwards compatibility
161
            if ($transform === null && ImageOptimize::$plugin->transformMethod instanceof ThumborImageTransform) {
162
                $transform = new AssetTransform([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
163
                    'width'     => $asset->width,
164
                    'interlace' => 'line',
165
                ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
166
            }
167
            // If we're passed an array, make an AssetTransform model out of it
168
            if (\is_array($transform)) {
169
                $transform = new AssetTransform($transform);
170
            }
171
            // If we're passing in a string, look up the asset transform in the db
172
            if (\is_string($transform)) {
173
                $assetTransforms = Craft::$app->getAssetTransforms();
174
                $transform = $assetTransforms->getTransformByHandle($transform);
175
            }
176
            // If the final format is an SVG, don't attempt to transform it
177
            $finalFormat = empty($transform['format']) ? $asset->getExtension() : $transform['format'];
178
            if ($finalFormat === 'svg') {
179
                return null;
180
            }
181
            // Normalize the extension to lowercase, for some transform methods that require this
182
            if (!empty($transform) && !empty($finalFormat)) {
183
                $format = $transform['format'] ?? null;
184
                $transform['format'] = $format === null ? null : strtolower($finalFormat);
185
            }
186
            // Generate an image transform url
187
            $url = ImageOptimize::$plugin->transformMethod->getTransformUrl(
188
                $asset,
189
                $transform
190
            );
191
        }
192
        Craft::endProfile('handleGetAssetUrlEvent', __METHOD__);
193
194
        return $url;
195
    }
196
197
    /**
198
     * Handle responding to EVENT_GET_ASSET_THUMB_URL events
199
     *
200
     * @param GetAssetThumbUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
201
     *
202
     * @return null|string
203
     */
204
    public function handleGetAssetThumbUrlEvent(GetAssetThumbUrlEvent $event)
205
    {
206
        Craft::beginProfile('handleGetAssetThumbUrlEvent', __METHOD__);
207
        $url = $event->url;
208
        if (!ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform) {
209
            $asset = $event->asset;
210
            if (ImageHelper::canManipulateAsImage($asset->getExtension())) {
211
                $transform = new AssetTransform([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
212
                    'width' => $event->width,
213
                    'height' => $event->height,
214
                    'interlace' => 'line',
215
                ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
216
                /** @var ImageTransform $transformMethod */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
217
                $transformMethod = ImageOptimize::$plugin->transformMethod;
218
                // If the final format is an SVG, don't attempt to transform it
219
                $finalFormat = empty($transform['format']) ? $asset->getExtension() : $transform['format'];
220
                if ($finalFormat === 'svg') {
221
                    return null;
222
                }
223
                // Normalize the extension to lowercase, for some transform methods that require this
224
                if ($transform !== null && !empty($finalFormat)) {
225
                    $transform['format'] = strtolower($finalFormat);
226
                }
227
                // Generate an image transform url
228
                if ($transformMethod->hasProperty('generateTransformsBeforePageLoad')) {
229
                    $transformMethod->generateTransformsBeforePageLoad = $event->generate;
0 ignored issues
show
Bug Best Practice introduced by
The property generateTransformsBeforePageLoad does not exist on nystudio107\imageoptimiz...ansforms\ImageTransform. Since you implemented __set, consider adding a @property annotation.
Loading history...
230
                }
231
                $url = $transformMethod->getTransformUrl($asset, $transform);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $url is correct as $transformMethod->getTra...Url($asset, $transform) targeting nystudio107\imageoptimiz...form::getTransformUrl() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
232
            }
233
        }
234
        Craft::endProfile('handleGetAssetThumbUrlEvent', __METHOD__);
235
236
        return $url;
237
    }
238
239
    /**
240
     * Handle responding to EVENT_GENERATE_TRANSFORM events
241
     *
242
     * @param GenerateTransformEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
243
     *
244
     * @return null|string
245
     */
246
    public function handleGenerateTransformEvent(GenerateTransformEvent $event)
247
    {
248
        Craft::beginProfile('handleGenerateTransformEvent', __METHOD__);
249
        $tempPath = null;
250
251
        // Only do this for local Craft transforms
252
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
253
            // Apply any filters to the image
254
            if ($event->transformIndex->transform !== null) {
255
                $this->applyFiltersToImage($event->transformIndex->transform, $event->asset, $event->image);
256
            }
257
            // Save the transformed image to a temp file
258
            $tempPath = $this->saveTransformToTempFile(
259
                $event->transformIndex,
260
                $event->image
261
            );
262
            $originalFileSize = @filesize($tempPath);
263
            // Optimize the image
264
            $this->optimizeImage(
265
                $event->transformIndex,
266
                $tempPath
267
            );
268
            clearstatcache(true, $tempPath);
269
            // Log the results of the image optimization
270
            $optimizedFileSize = @filesize($tempPath);
271
            $index = $event->transformIndex;
272
            Craft::info(
273
                pathinfo($index->filename, PATHINFO_FILENAME)
274
                .'.'
275
                .$index->detectedFormat
276
                .' -> '
277
                .Craft::t('image-optimize', 'Original')
278
                .': '
279
                .$this->humanFileSize($originalFileSize, 1)
280
                .', '
281
                .Craft::t('image-optimize', 'Optimized')
282
                .': '
283
                .$this->humanFileSize($optimizedFileSize, 1)
284
                .' -> '
285
                .Craft::t('image-optimize', 'Savings')
286
                .': '
287
                .number_format(abs(100 - (($optimizedFileSize * 100) / $originalFileSize)), 1)
288
                .'%',
289
                __METHOD__
290
            );
291
            // Create any image variants
292
            $this->createImageVariants(
293
                $event->transformIndex,
294
                $event->asset,
295
                $tempPath
296
            );
297
        }
298
        Craft::endProfile('handleGenerateTransformEvent', __METHOD__);
299
300
        return $tempPath;
301
    }
302
303
    /**
304
     * Handle cleaning up any variant creator images
305
     *
306
     * @param AssetTransformImageEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
307
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
308
    public function handleAfterDeleteTransformsEvent(AssetTransformImageEvent $event)
309
    {
310
        $settings = ImageOptimize::$plugin->getSettings();
0 ignored issues
show
Unused Code introduced by
The assignment to $settings is dead and can be removed.
Loading history...
311
        // Only do this for local Craft transforms
312
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
313
            $this->cleanupImageVariants($event->asset, $event->transformIndex);
314
        }
315
    }
316
317
    /**
318
     * Save out the image to a temp file
319
     *
320
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
321
     * @param Image               $image
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
322
     *
323
     * @return string
324
     */
325
    public function saveTransformToTempFile(AssetTransformIndex $index, Image $image): string
326
    {
327
        $tempFilename = uniqid(pathinfo($index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
328
        $tempPath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$tempFilename;
329
        try {
330
            $image->saveAs($tempPath);
331
        } catch (ImageException $e) {
332
            Craft::error('Transformed image save failed: '.$e->getMessage(), __METHOD__);
333
        }
334
        Craft::info('Transformed image saved to: '.$tempPath, __METHOD__);
335
336
        return $tempPath;
337
    }
338
339
    /**
340
     * Run any image post-processing/optimization on the image file
341
     *
342
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
343
     * @param string              $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
344
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
345
    public function optimizeImage(AssetTransformIndex $index, string $tempPath)
346
    {
347
        Craft::beginProfile('optimizeImage', __METHOD__);
348
        $settings = ImageOptimize::$plugin->getSettings();
349
        // Get the active processors for the transform format
350
        $activeImageProcessors = $settings->activeImageProcessors;
351
        $fileFormat = $index->detectedFormat;
352
        // Special-case for 'jpeg'
353
        if ($fileFormat === 'jpeg') {
354
            $fileFormat = 'jpg';
355
        }
356
        if (!empty($activeImageProcessors[$fileFormat])) {
357
            // Iterate through all of the processors for this format
358
            $imageProcessors = $settings->imageProcessors;
359
            foreach ($activeImageProcessors[$fileFormat] as $processor) {
360
                if (!empty($processor) && !empty($imageProcessors[$processor])) {
361
                    $this->executeImageProcessor($imageProcessors[$processor], $tempPath);
362
                }
363
            }
364
        }
365
        Craft::endProfile('optimizeImage', __METHOD__);
366
    }
367
368
    /**
369
     * Translate bytes into something human-readable
370
     *
371
     * @param     $bytes
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 5
Loading history...
372
     * @param int $decimals
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
373
     *
374
     * @return string
375
     */
376
    public function humanFileSize($bytes, $decimals = 1): string
377
    {
378
        $oldSize = Craft::$app->formatter->sizeFormatBase;
379
        Craft::$app->formatter->sizeFormatBase = 1000;
380
        $result = Craft::$app->formatter->asShortSize($bytes, $decimals);
381
        Craft::$app->formatter->sizeFormatBase = $oldSize;
382
383
        return $result;
384
    }
385
386
    /**
387
     * Create any image variants for the image file
388
     *
389
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
390
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
391
     * @param string              $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
392
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
393
    public function createImageVariants(AssetTransformIndex $index, Asset $asset, string $tempPath)
394
    {
395
        Craft::beginProfile('createImageVariants', __METHOD__);
396
        $settings = ImageOptimize::$plugin->getSettings();
397
        // Get the active image variant creators
398
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
399
        $fileFormat = $index->detectedFormat ?? $index->format;
400
        // Special-case for 'jpeg'
401
        if ($fileFormat === 'jpeg') {
402
            $fileFormat = 'jpg';
403
        }
404
        if (!empty($activeImageVariantCreators[$fileFormat])) {
405
            // Iterate through all of the image variant creators for this format
406
            $imageVariantCreators = $settings->imageVariantCreators;
407
            foreach ($activeImageVariantCreators[$fileFormat] as $variantCreator) {
408
                if (!empty($variantCreator) && !empty($imageVariantCreators[$variantCreator])) {
409
                    // Create the image variant in a temporary folder
410
                    $generalConfig = Craft::$app->getConfig()->getGeneral();
411
                    $quality = $index->transform->quality ?: $generalConfig->defaultImageQuality;
412
                    $outputPath = $this->executeVariantCreator(
413
                        $imageVariantCreators[$variantCreator],
414
                        $tempPath,
415
                        $quality
416
                    );
417
418
                    if ($outputPath !== null) {
419
                        // Get info on the original and the created variant
420
                        $originalFileSize = @filesize($tempPath);
421
                        $variantFileSize = @filesize($outputPath);
422
423
                        Craft::info(
424
                            pathinfo($tempPath, PATHINFO_FILENAME)
425
                            .'.'
426
                            .pathinfo($tempPath, PATHINFO_EXTENSION)
427
                            .' -> '
428
                            .pathinfo($outputPath, PATHINFO_FILENAME)
429
                            .'.'
430
                            .pathinfo($outputPath, PATHINFO_EXTENSION)
431
                            .' -> '
432
                            .Craft::t('image-optimize', 'Original')
433
                            .': '
434
                            .$this->humanFileSize($originalFileSize, 1)
435
                            .', '
436
                            .Craft::t('image-optimize', 'Variant')
437
                            .': '
438
                            .$this->humanFileSize($variantFileSize, 1)
439
                            .' -> '
440
                            .Craft::t('image-optimize', 'Savings')
441
                            .': '
442
                            .number_format(abs(100 - (($variantFileSize * 100) / $originalFileSize)), 1)
443
                            .'%',
444
                            __METHOD__
445
                        );
446
447
                        // Copy the image variant into place
448
                        $this->copyImageVariantToVolume(
449
                            $imageVariantCreators[$variantCreator],
450
                            $asset,
451
                            $index,
452
                            $outputPath
453
                        );
454
                    }
455
                }
456
            }
457
        }
458
        Craft::endProfile('createImageVariants', __METHOD__);
459
    }
460
461
    /**
462
     * Return an array of active image processors
463
     *
464
     * @return array
465
     */
466
    public function getActiveImageProcessors(): array
467
    {
468
        $result = [];
469
        $settings = ImageOptimize::$plugin->getSettings();
470
        // Get the active processors for the transform format
471
        $activeImageProcessors = $settings->activeImageProcessors;
472
        foreach ($activeImageProcessors as $imageFormat => $imageProcessor) {
473
            // Iterate through all of the processors for this format
474
            $imageProcessors = $settings->imageProcessors;
475
            foreach ($activeImageProcessors[$imageFormat] as $processor) {
476
                if (!empty($imageProcessors[$processor])) {
477
                    $thisImageProcessor = $imageProcessors[$processor];
478
                    $result[] = [
479
                        'format'    => $imageFormat,
480
                        'creator'   => $processor,
481
                        'command'   => $thisImageProcessor['commandPath']
482
                            .' '
483
                            .$thisImageProcessor['commandOptions'],
484
                        'installed' => is_file($thisImageProcessor['commandPath']),
485
                    ];
486
                }
487
            }
488
        }
489
490
        return $result;
491
    }
492
493
    /**
494
     * Return an array of active image variant creators
495
     *
496
     * @return array
497
     */
498
    public function getActiveVariantCreators(): array
499
    {
500
        $result = [];
501
        $settings = ImageOptimize::$plugin->getSettings();
502
        // Get the active image variant creators
503
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
504
        foreach ($activeImageVariantCreators as $imageFormat => $imageCreator) {
505
            // Iterate through all of the image variant creators for this format
506
            $imageVariantCreators = $settings->imageVariantCreators;
507
            foreach ($activeImageVariantCreators[$imageFormat] as $variantCreator) {
508
                if (!empty($imageVariantCreators[$variantCreator])) {
509
                    $thisVariantCreator = $imageVariantCreators[$variantCreator];
510
                    $result[] = [
511
                        'format'    => $imageFormat,
512
                        'creator'   => $variantCreator,
513
                        'command'   => $thisVariantCreator['commandPath']
514
                            .' '
515
                            .$thisVariantCreator['commandOptions'],
516
                        'installed' => is_file($thisVariantCreator['commandPath']),
517
                    ];
518
                }
519
            }
520
        }
521
522
        return $result;
523
    }
524
525
    /**
526
     * Returns whether `.webp` is a format supported by the server
527
     *
528
     * @return bool
529
     */
530
    public function serverSupportsWebP(): bool
531
    {
532
        $variantCreators = $this->getActiveVariantCreators();
533
        foreach ($variantCreators as $variantCreator) {
534
            if ($variantCreator['creator'] === 'cwebp' && $variantCreator['installed']) {
535
                return true;
536
            }
537
        }
538
539
        return false;
540
    }
541
542
    // Protected Methods
543
    // =========================================================================
544
545
    /** @noinspection PhpUnusedParameterInspection
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Tag @noinspection cannot be grouped with parameter tags in a doc comment
Loading history...
546
     * @param AssetTransform $transform
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 8 spaces but found 1
Loading history...
547
     * @param Asset          $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 8 spaces but found 1
Loading history...
548
     * @param Image          $image
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 8 spaces but found 1
Loading history...
549
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
550
    protected function applyFiltersToImage(AssetTransform $transform, Asset $asset, Image $image)
551
    {
552
        $settings = ImageOptimize::$plugin->getSettings();
553
        // Only try to apply filters to Raster images
554
        if ($image instanceof Raster) {
555
            $imagineImage = $image->getImagineImage();
556
            if ($imagineImage !== null) {
557
                // Handle auto-sharpening scaled down images
558
                if ($settings->autoSharpenScaledImages) {
559
                    // See if the image has been scaled >= 50%
560
                    $widthScale = $asset->getWidth() / $image->getWidth();
561
                    $heightScale = $asset->getHeight() / $image->getHeight();
562
                    if (($widthScale >= 2.0) || ($heightScale >= 2.0)) {
563
                        $imagineImage->effects()
564
                            ->sharpen();
565
                        Craft::debug(
566
                            Craft::t(
567
                                'image-optimize',
568
                                'Image transform >= 50%, sharpened the transformed image: {name}',
569
                                [
570
                                    'name' => $asset->title,
571
                                ]
572
                            ),
573
                            __METHOD__
574
                        );
575
                    }
576
                }
577
            }
578
        }
579
    }
580
581
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
582
     * @param string  $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 1 spaces after parameter type; 2 found
Loading history...
Coding Style introduced by
Doc comment for parameter $tempPath does not match actual variable name $thisProcessor
Loading history...
583
     * @param         $thisProcessor
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Doc comment for parameter $thisProcessor does not match actual variable name $tempPath
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 9
Loading history...
584
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
585
    protected function executeImageProcessor($thisProcessor, string $tempPath)
586
    {
587
        // Make sure the command exists
588
        if (is_file($thisProcessor['commandPath'])) {
589
            // Set any options for the command
590
            $commandOptions = '';
591
            if (!empty($thisProcessor['commandOptions'])) {
592
                $commandOptions = ' '
593
                    .$thisProcessor['commandOptions']
594
                    .' ';
595
            }
596
            // Redirect the command output if necessary for this processor
597
            $outputFileFlag = '';
598
            if (!empty($thisProcessor['commandOutputFileFlag'])) {
599
                $outputFileFlag = ' '
600
                    .$thisProcessor['commandOutputFileFlag']
601
                    .' '
602
                    .escapeshellarg($tempPath)
603
                    .' ';
604
            }
605
            // Build the command to execute
606
            $cmd =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
607
                $thisProcessor['commandPath']
608
                .$commandOptions
609
                .$outputFileFlag
610
                .escapeshellarg($tempPath);
611
            // Execute the command
612
            $shellOutput = $this->executeShellCommand($cmd);
613
            Craft::info($cmd."\n".$shellOutput, __METHOD__);
614
        } else {
615
            Craft::error(
616
                $thisProcessor['commandPath']
617
                .' '
618
                .Craft::t('image-optimize', 'does not exist'),
619
                __METHOD__
620
            );
621
        }
622
    }
623
624
    /**
625
     * Execute a shell command
626
     *
627
     * @param string $command
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
628
     *
629
     * @return string
630
     */
631
    protected function executeShellCommand(string $command): string
632
    {
633
        // Create the shell command
634
        $shellCommand = new ShellCommand();
635
        $shellCommand->setCommand($command);
636
637
        // If we don't have proc_open, maybe we've got exec
638
        if (!\function_exists('proc_open') && \function_exists('exec')) {
639
            $shellCommand->useExec = true;
640
        }
641
642
        // Return the result of the command's output or error
643
        if ($shellCommand->execute()) {
644
            $result = $shellCommand->getOutput();
645
        } else {
646
            $result = $shellCommand->getError();
647
        }
648
649
        return $result;
650
    }
651
652
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
653
     * @param         $variantCreatorCommand
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 9
Loading history...
654
     * @param string  $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 1 spaces after parameter type; 2 found
Loading history...
655
     * @param int     $imageQuality
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 4 spaces after parameter type; 5 found
Loading history...
656
     *
657
     * @return string|null the path to the created variant
658
     */
659
    protected function executeVariantCreator($variantCreatorCommand, string $tempPath, int $imageQuality)
660
    {
661
        $outputPath = $tempPath;
662
        // Make sure the command exists
663
        if (is_file($variantCreatorCommand['commandPath'])) {
664
            // Get the output file for this image variant
665
            $outputPath .= '.'.$variantCreatorCommand['imageVariantExtension'];
666
            // Set any options for the command
667
            $commandOptions = '';
668
            if (!empty($variantCreatorCommand['commandOptions'])) {
669
                $commandOptions = ' '
670
                    .$variantCreatorCommand['commandOptions']
671
                    .' ';
672
            }
673
            // Redirect the command output if necessary for this variantCreator
674
            $outputFileFlag = '';
675
            if (!empty($variantCreatorCommand['commandOutputFileFlag'])) {
676
                $outputFileFlag = ' '
677
                    .$variantCreatorCommand['commandOutputFileFlag']
678
                    .' '
679
                    .escapeshellarg($outputPath)
680
                    .' ';
681
            }
682
            // Get the quality setting of this transform
683
            $commandQualityFlag = '';
684
            if (!empty($variantCreatorCommand['commandQualityFlag'])) {
685
                $commandQualityFlag = ' '
686
                    .$variantCreatorCommand['commandQualityFlag']
687
                    .' '
688
                    .$imageQuality
689
                    .' ';
690
            }
691
            // Build the command to execute
692
            $cmd =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
693
                $variantCreatorCommand['commandPath']
694
                .$commandOptions
695
                .$commandQualityFlag
696
                .$outputFileFlag
697
                .escapeshellarg($tempPath);
698
            // Execute the command
699
            $shellOutput = $this->executeShellCommand($cmd);
700
            Craft::info($cmd."\n".$shellOutput, __METHOD__);
701
        } else {
702
            Craft::error(
703
                $variantCreatorCommand['commandPath']
704
                .' '
705
                .Craft::t('image-optimize', 'does not exist'),
706
                __METHOD__
707
            );
708
            $outputPath = null;
709
        }
710
711
        return $outputPath;
712
    }
713
714
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
715
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
716
     * @param AssetTransformIndex $transformIndex
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
717
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
718
    protected function cleanupImageVariants(Asset $asset, AssetTransformIndex $transformIndex)
719
    {
720
        $settings = ImageOptimize::$plugin->getSettings();
721
        $assetTransforms = Craft::$app->getAssetTransforms();
722
        // Get the active image variant creators
723
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
724
        $fileFormat = $transformIndex->detectedFormat ?? $transformIndex->format;
725
        if (!empty($activeImageVariantCreators[$fileFormat])) {
726
            // Iterate through all of the image variant creators for this format
727
            $imageVariantCreators = $settings->imageVariantCreators;
728
            if (!empty($activeImageVariantCreators[$fileFormat])) {
729
                foreach ($activeImageVariantCreators[$fileFormat] as $variantCreator) {
730
                    if (!empty($variantCreator) && !empty($imageVariantCreators[$variantCreator])) {
731
                        // Create the image variant in a temporary folder
732
                        $variantCreatorCommand = $imageVariantCreators[$variantCreator];
733
                        try {
734
                            $volume = $asset->getVolume();
735
                        } catch (InvalidConfigException $e) {
736
                            $volume = null;
737
                            Craft::error(
738
                                'Asset volume error: '.$e->getMessage(),
739
                                __METHOD__
740
                            );
741
                        }
742
                        try {
743
                            $variantPath = $asset->getFolder()->path.$assetTransforms->getTransformSubpath(
744
                                $asset,
745
                                $transformIndex
746
                            );
747
                        } catch (InvalidConfigException $e) {
748
                            $variantPath = '';
749
                            Craft::error(
750
                                'Asset folder does not exist: '.$e->getMessage(),
751
                                __METHOD__
752
                            );
753
                        }
754
                        $variantPath .= '.'.$variantCreatorCommand['imageVariantExtension'];
755
                        // Delete the variant file in case it is stale
756
                        try {
757
                            $volume->deleteFile($variantPath);
758
                        } catch (VolumeException $e) {
759
                            // We're fine with that.
760
                        }
761
                        Craft::info(
762
                            'Deleted variant: '.$variantPath,
763
                            __METHOD__
764
                        );
765
                    }
766
                }
767
            }
768
        }
769
    }
770
771
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
772
     * @param                     $variantCreatorCommand
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 21
Loading history...
773
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
774
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
775
     * @param                     $outputPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 21
Loading history...
776
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
777
    protected function copyImageVariantToVolume(
778
        $variantCreatorCommand,
779
        Asset $asset,
780
        AssetTransformIndex $index,
781
        $outputPath
782
    ) {
783
        // If the image variant creation succeeded, copy it into place
784
        if (!empty($outputPath) && is_file($outputPath)) {
785
            // Figure out the resulting path for the image variant
786
            try {
787
                $volume = $asset->getVolume();
788
            } catch (InvalidConfigException $e) {
789
                $volume = null;
790
                Craft::error(
791
                    'Asset volume error: '.$e->getMessage(),
792
                    __METHOD__
793
                );
794
            }
795
            $assetTransforms = Craft::$app->getAssetTransforms();
796
            try {
797
                $transformPath = $asset->getFolder()->path.$assetTransforms->getTransformSubpath($asset, $index);
798
            } catch (InvalidConfigException $e) {
799
                $transformPath = '';
800
                Craft::error(
801
                    'Error getting asset folder: '.$e->getMessage(),
802
                    __METHOD__
803
                );
804
            }
805
            $variantPath = $transformPath.'.'.$variantCreatorCommand['imageVariantExtension'];
806
807
            // Delete the variant file in case it is stale
808
            try {
809
                $volume->deleteFile($variantPath);
810
            } catch (VolumeException $e) {
811
                // We're fine with that.
812
            }
813
814
            Craft::info(
815
                'Variant output path: '.$outputPath.' - Variant path: '.$variantPath,
816
                __METHOD__
817
            );
818
819
            clearstatcache(true, $outputPath);
820
            $stream = @fopen($outputPath, 'rb');
821
            if ($stream !== false) {
822
                // Now create it
823
                try {
824
                    $volume->createFileByStream($variantPath, $stream, []);
825
                } catch (VolumeException $e) {
826
                    Craft::error(
827
                        Craft::t('image-optimize', 'Failed to create image variant at: ')
828
                        .$outputPath,
829
                        __METHOD__
830
                    );
831
                }
832
833
                FileHelper::unlink($outputPath);
834
            }
835
        } else {
836
            Craft::error(
837
                Craft::t('image-optimize', 'Failed to create image variant at: ')
838
                .$outputPath,
839
                __METHOD__
840
            );
841
        }
842
    }
843
844
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
845
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
846
     * @param string $extension
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
847
     *
848
     * @return string
849
     */
850
    protected function swapPathExtension(string $path, string $extension): string
851
    {
852
        $pathParts = pathinfo($path);
853
        $newPath = $pathParts['filename'].'.'.$extension;
854
        if (!empty($pathParts['dirname']) && $pathParts['dirname'] !== '.') {
855
            $newPath = $pathParts['dirname'].DIRECTORY_SEPARATOR.$newPath;
856
            $newPath = preg_replace('#/+#', '/', $newPath);
857
        }
858
859
        return $newPath;
860
    }
861
}
862