Passed
Pull Request — develop (#192)
by
unknown
06:35
created

Optimize::handleGetAssetUrlEvent()   B

Complexity

Conditions 11
Paths 51

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 29
c 5
b 0
f 0
dl 0
loc 50
rs 7.3166
cc 11
nc 51
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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