Passed
Push — develop ( c9b60f...ceb073 )
by Andrew
06:56
created

Optimize::cleanupImageVariants()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 46
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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