Passed
Push — develop ( 3790c4...67d357 )
by Andrew
05:08
created

Optimize::getActiveVariantCreators()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 25
rs 9.7333
c 0
b 0
f 0
cc 4
nc 4
nop 0
1
<?php
2
/**
3
 * ImageOptimize plugin for Craft CMS 3.x
4
 *
5
 * Automatically optimize images after they've been transformed
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\imageoptimize\services;
12
13
use nystudio107\imageoptimize\ImageOptimize;
14
use nystudio107\imageoptimize\imagetransforms\CraftImageTransform;
15
use nystudio107\imageoptimize\imagetransforms\ImageTransform;
16
use nystudio107\imageoptimize\imagetransforms\ImageTransformInterface;
17
use nystudio107\imageoptimize\imagetransforms\ImgixImageTransform;
18
use nystudio107\imageoptimize\imagetransforms\ThumborImageTransform;
19
20
use Craft;
21
use craft\base\Component;
22
use craft\base\Image;
23
use craft\elements\Asset;
24
use craft\errors\ImageException;
25
use craft\errors\VolumeException;
26
use craft\events\AssetTransformImageEvent;
27
use craft\events\GetAssetThumbUrlEvent;
28
use craft\events\GetAssetUrlEvent;
29
use craft\events\GenerateTransformEvent;
30
use craft\events\RegisterComponentTypesEvent;
31
use craft\helpers\Component as ComponentHelper;
32
use craft\helpers\FileHelper;
33
use craft\helpers\Assets as AssetsHelper;
34
use craft\helpers\Image as ImageHelper;
35
use craft\image\Raster;
36
use craft\models\AssetTransform;
37
use craft\models\AssetTransformIndex;
38
39
use mikehaertl\shellcommand\Command as ShellCommand;
40
41
use yii\base\InvalidConfigException;
42
43
/** @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...
44
45
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
46
 * @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 indented incorrectly; expected 2 spaces but found 4
Loading history...
47
 * @package   ImageOptimize
0 ignored issues
show
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 3
Loading history...
48
 * @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 indented incorrectly; expected 3 spaces but found 5
Loading history...
49
 */
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...
50
class Optimize extends Component
51
{
52
    // Constants
53
    // =========================================================================
54
55
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
56
     * @event RegisterComponentTypesEvent The event that is triggered when registering
57
     *        Image Transform types
58
     *
59
     * Image Transform types must implement [[ImageTransformInterface]]. [[ImageTransform]]
60
     * provides a base implementation.
61
     *
62
     * ```php
63
     * use nystudio107\imageoptimize\services\Optimize;
64
     * use craft\events\RegisterComponentTypesEvent;
65
     * use yii\base\Event;
66
     *
67
     * Event::on(Optimize::class,
68
     *     Optimize::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES,
69
     *     function(RegisterComponentTypesEvent $event) {
70
     *         $event->types[] = MyImageTransform::class;
71
     *     }
72
     * );
73
     * ```
74
     */
75
    const EVENT_REGISTER_IMAGE_TRANSFORM_TYPES = 'registerImageTransformTypes';
76
77
    const DEFAULT_IMAGE_TRANSFORM_TYPES = [
78
        CraftImageTransform::class,
79
        ImgixImageTransform::class,
80
        ThumborImageTransform::class,
81
    ];
82
83
    // Public Methods
84
    // =========================================================================
85
86
    /**
87
     * Returns all available field type classes.
88
     *
89
     * @return string[] The available field type classes
90
     */
91
    public function getAllImageTransformTypes(): array
92
    {
93
        $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...
94
            ImageOptimize::$plugin->getSettings()->defaultImageTransformTypes ?? [],
95
            self::DEFAULT_IMAGE_TRANSFORM_TYPES
96
        ), 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...
97
98
        $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...
99
            'types' => $imageTransformTypes
100
        ]);
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...
101
        $this->trigger(self::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES, $event);
102
103
        return $event->types;
104
    }
105
106
    /**
107
     * Creates an Image Transform with a given config.
108
     *
109
     * @param mixed $config The Image Transform’s class name, or its config,
110
     *                      with a `type` value and optionally a `settings` value
111
     *
112
     * @return null|ImageTransformInterface The Image Transform
113
     */
114
    public function createImageTransformType($config): ImageTransformInterface
115
    {
116
        if (is_string($config)) {
117
            $config = ['type' => $config];
118
        }
119
120
        try {
121
            /** @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...
122
            $imageTransform = ComponentHelper::createComponent($config, ImageTransformInterface::class);
123
        } catch (\Throwable $e) {
124
            $imageTransform = null;
125
            Craft::error($e->getMessage(), __METHOD__);
126
        }
127
128
        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...
129
    }
130
131
    /**
132
     * Handle responding to EVENT_GET_ASSET_URL events
133
     *
134
     * @param GetAssetUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
135
     *
136
     * @return null|string
137
     * @throws InvalidConfigException
138
     */
139
    public function handleGetAssetUrlEvent(GetAssetUrlEvent $event)
140
    {
141
        Craft::beginProfile('handleGetAssetUrlEvent', __METHOD__);
142
        $url = null;
143
        if (!ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform) {
144
            $asset = $event->asset;
145
            $transform = $event->transform;
146
            // If there's no transform requested, and we can't manipulate the image anyway, just return the URL
147
            if ($transform === null
148
                || !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...
149
                $volume = $asset->getVolume();
150
151
                return AssetsHelper::generateUrl($volume, $asset);
152
            }
153
            // If we're passed in null, make a dummy AssetTransform model
154
            if (empty($transform)) {
155
                $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...
156
                    'height'    => $asset->height,
157
                    'width'     => $asset->width,
158
                    'interlace' => 'line',
159
                ]);
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...
160
            }
161
            // If we're passed an array, make an AssetTransform model out of it
162
            if (\is_array($transform)) {
163
                $transform = new AssetTransform($transform);
164
            }
165
            // If we're passing in a string, look up the asset transform in the db
166
            if (\is_string($transform)) {
167
                $assetTransforms = Craft::$app->getAssetTransforms();
168
                $transform = $assetTransforms->getTransformByHandle($transform);
169
            }
170
            // Generate an image transform url
171
            $url = ImageOptimize::$plugin->transformMethod->getTransformUrl(
172
                $asset,
173
                $transform,
174
                ImageOptimize::$transformParams
175
            );
176
        }
177
        Craft::endProfile('handleGetAssetUrlEvent', __METHOD__);
178
179
        return $url;
180
    }
181
182
    /**
183
     * Handle responding to EVENT_GET_ASSET_THUMB_URL events
184
     *
185
     * @param GetAssetThumbUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
186
     *
187
     * @return null|string
188
     */
189
    public function handleGetAssetThumbUrlEvent(GetAssetThumbUrlEvent $event)
190
    {
191
        Craft::beginProfile('handleGetAssetThumbUrlEvent', __METHOD__);
192
        $url = null;
193
        if (!ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform) {
194
            $asset = $event->asset;
195
            $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...
196
                'height'    => $event->height,
197
                'width'     => $event->width,
198
                'interlace' => 'line',
199
            ]);
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...
200
            // Generate an image transform url
201
            ImageOptimize::$transformParams['generateTransformsBeforePageLoad'] = $event->generate;
202
            $url = ImageOptimize::$plugin->transformMethod->getTransformUrl(
203
                $asset,
204
                $transform,
205
                ImageOptimize::$transformParams
206
            );
207
        }
208
        Craft::endProfile('handleGetAssetThumbUrlEvent', __METHOD__);
209
210
        return $url;
211
    }
212
213
    /**
214
     * Handle responding to EVENT_GENERATE_TRANSFORM events
215
     *
216
     * @param GenerateTransformEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
217
     *
218
     * @return null|string
219
     */
220
    public function handleGenerateTransformEvent(GenerateTransformEvent $event)
221
    {
222
        Craft::beginProfile('handleGenerateTransformEvent', __METHOD__);
223
        $tempPath = null;
224
225
        // Only do this for local Craft transforms
226
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
227
            // Apply any filters to the image
228
            if ($event->transformIndex->transform !== null) {
229
                $this->applyFiltersToImage($event->transformIndex->transform, $event->asset, $event->image);
230
            }
231
            // Save the transformed image to a temp file
232
            $tempPath = $this->saveTransformToTempFile(
233
                $event->transformIndex,
234
                $event->image
235
            );
236
            $originalFileSize = @filesize($tempPath);
237
            // Optimize the image
238
            $this->optimizeImage(
239
                $event->transformIndex,
240
                $tempPath
241
            );
242
            clearstatcache(true, $tempPath);
243
            // Log the results of the image optimization
244
            $optimizedFileSize = @filesize($tempPath);
245
            $index = $event->transformIndex;
246
            Craft::info(
247
                pathinfo($index->filename, PATHINFO_FILENAME)
248
                .'.'
249
                .$index->detectedFormat
250
                .' -> '
251
                .Craft::t('image-optimize', 'Original')
252
                .': '
253
                .$this->humanFileSize($originalFileSize, 1)
254
                .', '
255
                .Craft::t('image-optimize', 'Optimized')
256
                .': '
257
                .$this->humanFileSize($optimizedFileSize, 1)
258
                .' -> '
259
                .Craft::t('image-optimize', 'Savings')
260
                .': '
261
                .number_format(abs(100 - (($optimizedFileSize * 100) / $originalFileSize)), 1)
262
                .'%',
263
                __METHOD__
264
            );
265
            // Create any image variants
266
            $this->createImageVariants(
267
                $event->transformIndex,
268
                $event->asset,
269
                $tempPath
270
            );
271
        }
272
        Craft::endProfile('handleGenerateTransformEvent', __METHOD__);
273
274
        return $tempPath;
275
    }
276
277
    /**
278
     * Handle cleaning up any variant creator images
279
     *
280
     * @param AssetTransformImageEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
281
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
282
    public function handleAfterDeleteTransformsEvent(AssetTransformImageEvent $event)
283
    {
284
        $settings = ImageOptimize::$plugin->getSettings();
0 ignored issues
show
Unused Code introduced by
The assignment to $settings is dead and can be removed.
Loading history...
285
        // Only do this for local Craft transforms
286
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
287
            $this->cleanupImageVariants($event->asset, $event->transformIndex);
288
        }
289
    }
290
291
    /**
292
     * Save out the image to a temp file
293
     *
294
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
295
     * @param Image               $image
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
296
     *
297
     * @return string
298
     */
299
    public function saveTransformToTempFile(AssetTransformIndex $index, Image $image): string
300
    {
301
        $tempFilename = uniqid(pathinfo($index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
302
        $tempPath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$tempFilename;
303
        try {
304
            $image->saveAs($tempPath);
305
        } catch (ImageException $e) {
306
            Craft::error('Transformed image save failed: '.$e->getMessage(), __METHOD__);
307
        }
308
        Craft::info('Transformed image saved to: '.$tempPath, __METHOD__);
309
310
        return $tempPath;
311
    }
312
313
    /**
314
     * Run any image post-processing/optimization on the image file
315
     *
316
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
317
     * @param string              $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
318
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
319
    public function optimizeImage(AssetTransformIndex $index, string $tempPath)
320
    {
321
        Craft::beginProfile('optimizeImage', __METHOD__);
322
        $settings = ImageOptimize::$plugin->getSettings();
323
        // Get the active processors for the transform format
324
        $activeImageProcessors = $settings->activeImageProcessors;
325
        $fileFormat = $index->detectedFormat;
326
        // Special-case for 'jpeg'
327
        if ($fileFormat === 'jpeg') {
328
            $fileFormat = 'jpg';
329
        }
330
        if (!empty($activeImageProcessors[$fileFormat])) {
331
            // Iterate through all of the processors for this format
332
            $imageProcessors = $settings->imageProcessors;
333
            foreach ($activeImageProcessors[$fileFormat] as $processor) {
334
                if (!empty($processor) && !empty($imageProcessors[$processor])) {
335
                    $this->executeImageProcessor($imageProcessors[$processor], $tempPath);
336
                }
337
            }
338
        }
339
        Craft::endProfile('optimizeImage', __METHOD__);
340
    }
341
342
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $bytes should have a doc-comment as per coding-style.
Loading history...
343
     * Translate bytes into something human-readable
344
     *
345
     * @param     $bytes
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 5
Loading history...
346
     * @param int $decimals
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
347
     *
348
     * @return string
349
     */
350
    public function humanFileSize($bytes, $decimals = 1): string
351
    {
352
        $oldSize = Craft::$app->formatter->sizeFormatBase;
353
        Craft::$app->formatter->sizeFormatBase = 1000;
354
        $result = Craft::$app->formatter->asShortSize($bytes, $decimals);
355
        Craft::$app->formatter->sizeFormatBase = $oldSize;
356
357
        return $result;
358
    }
359
360
    /**
361
     * Create any image variants for the image file
362
     *
363
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
364
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
365
     * @param string              $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
366
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
367
    public function createImageVariants(AssetTransformIndex $index, Asset $asset, string $tempPath)
368
    {
369
        Craft::beginProfile('createImageVariants', __METHOD__);
370
        $settings = ImageOptimize::$plugin->getSettings();
371
        // Get the active image variant creators
372
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
373
        $fileFormat = $index->detectedFormat ?? $index->format;
374
        // Special-case for 'jpeg'
375
        if ($fileFormat === 'jpeg') {
376
            $fileFormat = 'jpg';
377
        }
378
        if (!empty($activeImageVariantCreators[$fileFormat])) {
379
            // Iterate through all of the image variant creators for this format
380
            $imageVariantCreators = $settings->imageVariantCreators;
381
            foreach ($activeImageVariantCreators[$fileFormat] as $variantCreator) {
382
                if (!empty($variantCreator) && !empty($imageVariantCreators[$variantCreator])) {
383
                    // Create the image variant in a temporary folder
384
                    $generalConfig = Craft::$app->getConfig()->getGeneral();
385
                    $quality = $index->transform->quality ?: $generalConfig->defaultImageQuality;
386
                    $outputPath = $this->executeVariantCreator(
387
                        $imageVariantCreators[$variantCreator],
388
                        $tempPath,
389
                        $quality
390
                    );
391
392
                    if ($outputPath !== null) {
393
                        // Get info on the original and the created variant
394
                        $originalFileSize = @filesize($tempPath);
395
                        $variantFileSize = @filesize($outputPath);
396
397
                        Craft::info(
398
                            pathinfo($tempPath, PATHINFO_FILENAME)
399
                            .'.'
400
                            .pathinfo($tempPath, PATHINFO_EXTENSION)
401
                            .' -> '
402
                            .pathinfo($outputPath, PATHINFO_FILENAME)
403
                            .'.'
404
                            .pathinfo($outputPath, PATHINFO_EXTENSION)
405
                            .' -> '
406
                            .Craft::t('image-optimize', 'Original')
407
                            .': '
408
                            .$this->humanFileSize($originalFileSize, 1)
409
                            .', '
410
                            .Craft::t('image-optimize', 'Variant')
411
                            .': '
412
                            .$this->humanFileSize($variantFileSize, 1)
413
                            .' -> '
414
                            .Craft::t('image-optimize', 'Savings')
415
                            .': '
416
                            .number_format(abs(100 - (($variantFileSize * 100) / $originalFileSize)), 1)
417
                            .'%',
418
                            __METHOD__
419
                        );
420
421
                        // Copy the image variant into place
422
                        $this->copyImageVariantToVolume(
423
                            $imageVariantCreators[$variantCreator],
424
                            $asset,
425
                            $index,
426
                            $outputPath
427
                        );
428
                    }
429
                }
430
            }
431
        }
432
        Craft::endProfile('createImageVariants', __METHOD__);
433
    }
434
435
    /**
436
     * Return an array of active image processors
437
     *
438
     * @return array
439
     */
440
    public function getActiveImageProcessors(): array
441
    {
442
        $result = [];
443
        $settings = ImageOptimize::$plugin->getSettings();
444
        // Get the active processors for the transform format
445
        $activeImageProcessors = $settings->activeImageProcessors;
446
        foreach ($activeImageProcessors as $imageFormat => $imageProcessor) {
447
            // Iterate through all of the processors for this format
448
            $imageProcessors = $settings->imageProcessors;
449
            foreach ($activeImageProcessors[$imageFormat] as $processor) {
450
                if (!empty($imageProcessors[$processor])) {
451
                    $thisImageProcessor = $imageProcessors[$processor];
452
                    $result[] = [
453
                        'format'    => $imageFormat,
454
                        'creator'   => $processor,
455
                        'command'   => $thisImageProcessor['commandPath']
456
                            .' '
457
                            .$thisImageProcessor['commandOptions'],
458
                        'installed' => is_file($thisImageProcessor['commandPath']),
459
                    ];
460
                }
461
            }
462
        }
463
464
        return $result;
465
    }
466
467
    /**
468
     * Return an array of active image variant creators
469
     *
470
     * @return array
471
     */
472
    public function getActiveVariantCreators(): array
473
    {
474
        $result = [];
475
        $settings = ImageOptimize::$plugin->getSettings();
476
        // Get the active image variant creators
477
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
478
        foreach ($activeImageVariantCreators as $imageFormat => $imageCreator) {
479
            // Iterate through all of the image variant creators for this format
480
            $imageVariantCreators = $settings->imageVariantCreators;
481
            foreach ($activeImageVariantCreators[$imageFormat] as $variantCreator) {
482
                if (!empty($imageVariantCreators[$variantCreator])) {
483
                    $thisVariantCreator = $imageVariantCreators[$variantCreator];
484
                    $result[] = [
485
                        'format'    => $imageFormat,
486
                        'creator'   => $variantCreator,
487
                        'command'   => $thisVariantCreator['commandPath']
488
                            .' '
489
                            .$thisVariantCreator['commandOptions'],
490
                        'installed' => is_file($thisVariantCreator['commandPath']),
491
                    ];
492
                }
493
            }
494
        }
495
496
        return $result;
497
    }
498
499
    // Protected Methods
500
    // =========================================================================
501
502
    /** @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 cannot be grouped with parameter tags in a doc comment
Loading history...
503
     * @param AssetTransform $transform
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 8 spaces but found 1
Loading history...
504
     * @param Asset          $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 8 spaces but found 1
Loading history...
505
     * @param Image          $image
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 8 spaces but found 1
Loading history...
506
     */
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...
507
508
    protected function applyFiltersToImage(AssetTransform $transform, Asset $asset, Image $image)
509
    {
510
        $settings = ImageOptimize::$plugin->getSettings();
511
        // Only try to apply filters to Raster images
512
        if ($image instanceof Raster) {
513
            $imagineImage = $image->getImagineImage();
514
            if ($imagineImage !== null) {
515
                // Handle auto-sharpening scaled down images
516
                if ($settings->autoSharpenScaledImages) {
517
                    // See if the image has been scaled >= 50%
518
                    $widthScale = $asset->getWidth() / $image->getWidth();
519
                    $heightScale = $asset->getHeight() / $image->getHeight();
520
                    if (($widthScale >= 2.0) || ($heightScale >= 2.0)) {
521
                        $imagineImage->effects()
522
                            ->sharpen();
523
                        Craft::debug(
524
                            Craft::t(
525
                                'image-optimize',
526
                                'Image transform >= 50%, sharpened the transformed image: {name}',
527
                                [
528
                                    'name' => $asset->title,
529
                                ]
530
                            ),
531
                            __METHOD__
532
                        );
533
                    }
534
                }
535
            }
536
        }
537
    }
538
539
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Parameter $thisProcessor should have a doc-comment as per coding-style.
Loading history...
540
     * @param string  $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 8 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...
541
     * @param         $thisProcessor
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 9
Loading history...
542
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
543
    protected function executeImageProcessor($thisProcessor, string $tempPath)
544
    {
545
        // Make sure the command exists
546
        if (is_file($thisProcessor['commandPath'])) {
547
            // Set any options for the command
548
            $commandOptions = '';
549
            if (!empty($thisProcessor['commandOptions'])) {
550
                $commandOptions = ' '
551
                    .$thisProcessor['commandOptions']
552
                    .' ';
553
            }
554
            // Redirect the command output if necessary for this processor
555
            $outputFileFlag = '';
556
            if (!empty($thisProcessor['commandOutputFileFlag'])) {
557
                $outputFileFlag = ' '
558
                    .$thisProcessor['commandOutputFileFlag']
559
                    .' '
560
                    .escapeshellarg($tempPath)
561
                    .' ';
562
            }
563
            // Build the command to execute
564
            $cmd =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
565
                $thisProcessor['commandPath']
566
                .$commandOptions
567
                .$outputFileFlag
568
                .escapeshellarg($tempPath);
569
            // Execute the command
570
            $shellOutput = $this->executeShellCommand($cmd);
571
            Craft::info($cmd."\n".$shellOutput, __METHOD__);
572
        } else {
573
            Craft::error(
574
                $thisProcessor['commandPath']
575
                .' '
576
                .Craft::t('image-optimize', 'does not exist'),
577
                __METHOD__
578
            );
579
        }
580
    }
581
582
    /**
583
     * Execute a shell command
584
     *
585
     * @param string $command
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
586
     *
587
     * @return string
588
     */
589
    protected function executeShellCommand(string $command): string
590
    {
591
        // Create the shell command
592
        $shellCommand = new ShellCommand();
593
        $shellCommand->setCommand($command);
594
595
        // If we don't have proc_open, maybe we've got exec
596
        if (!\function_exists('proc_open') && \function_exists('exec')) {
597
            $shellCommand->useExec = true;
598
        }
599
600
        // Return the result of the command's output or error
601
        if ($shellCommand->execute()) {
602
            $result = $shellCommand->getOutput();
603
        } else {
604
            $result = $shellCommand->getError();
605
        }
606
607
        return $result;
608
    }
609
610
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Parameter $variantCreatorCommand should have a doc-comment as per coding-style.
Loading history...
611
     * @param         $variantCreatorCommand
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 9
Loading history...
612
     * @param string  $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 16 spaces after parameter type; 2 found
Loading history...
613
     * @param int     $imageQuality
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 19 spaces after parameter type; 5 found
Loading history...
614
     *
615
     * @return string|null the path to the created variant
616
     */
617
    protected function executeVariantCreator($variantCreatorCommand, string $tempPath, int $imageQuality)
618
    {
619
        $outputPath = $tempPath;
620
        // Make sure the command exists
621
        if (is_file($variantCreatorCommand['commandPath'])) {
622
            // Get the output file for this image variant
623
            $outputPath .= '.'.$variantCreatorCommand['imageVariantExtension'];
624
            // Set any options for the command
625
            $commandOptions = '';
626
            if (!empty($variantCreatorCommand['commandOptions'])) {
627
                $commandOptions = ' '
628
                    .$variantCreatorCommand['commandOptions']
629
                    .' ';
630
            }
631
            // Redirect the command output if necessary for this variantCreator
632
            $outputFileFlag = '';
633
            if (!empty($variantCreatorCommand['commandOutputFileFlag'])) {
634
                $outputFileFlag = ' '
635
                    .$variantCreatorCommand['commandOutputFileFlag']
636
                    .' '
637
                    .escapeshellarg($outputPath)
638
                    .' ';
639
            }
640
            // Get the quality setting of this transform
641
            $commandQualityFlag = '';
642
            if (!empty($variantCreatorCommand['commandQualityFlag'])) {
643
                $commandQualityFlag = ' '
644
                    .$variantCreatorCommand['commandQualityFlag']
645
                    .' '
646
                    .$imageQuality
647
                    .' ';
648
            }
649
            // Build the command to execute
650
            $cmd =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
651
                $variantCreatorCommand['commandPath']
652
                .$commandOptions
653
                .$commandQualityFlag
654
                .$outputFileFlag
655
                .escapeshellarg($tempPath);
656
            // Execute the command
657
            $shellOutput = $this->executeShellCommand($cmd);
658
            Craft::info($cmd."\n".$shellOutput, __METHOD__);
659
        } else {
660
            Craft::error(
661
                $variantCreatorCommand['commandPath']
662
                .' '
663
                .Craft::t('image-optimize', 'does not exist'),
664
                __METHOD__
665
            );
666
            $outputPath = null;
667
        }
668
669
        return $outputPath;
670
    }
671
672
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
673
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
674
     * @param AssetTransformIndex $transformIndex
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
675
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
676
    protected function cleanupImageVariants(Asset $asset, AssetTransformIndex $transformIndex)
677
    {
678
        $settings = ImageOptimize::$plugin->getSettings();
679
        $assetTransforms = Craft::$app->getAssetTransforms();
680
        // Get the active image variant creators
681
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
682
        $fileFormat = $transformIndex->detectedFormat ?? $transformIndex->format;
683
        if (!empty($activeImageVariantCreators[$fileFormat])) {
684
            // Iterate through all of the image variant creators for this format
685
            $imageVariantCreators = $settings->imageVariantCreators;
686
            if (!empty($activeImageVariantCreators[$fileFormat])) {
687
                foreach ($activeImageVariantCreators[$fileFormat] as $variantCreator) {
688
                    if (!empty($variantCreator) && !empty($imageVariantCreators[$variantCreator])) {
689
                        // Create the image variant in a temporary folder
690
                        $variantCreatorCommand = $imageVariantCreators[$variantCreator];
691
                        try {
692
                            $volume = $asset->getVolume();
693
                        } catch (InvalidConfigException $e) {
694
                            $volume = null;
695
                            Craft::error(
696
                                'Asset volume error: '.$e->getMessage(),
697
                                __METHOD__
698
                            );
699
                        }
700
                        try {
701
                            $variantPath = $asset->getFolder()->path.$assetTransforms->getTransformSubpath(
702
                                $asset,
703
                                $transformIndex
704
                            );
705
                        } catch (InvalidConfigException $e) {
706
                            $variantPath = '';
707
                            Craft::error(
708
                                'Asset folder does not exist: '.$e->getMessage(),
709
                                __METHOD__
710
                            );
711
                        }
712
                        $variantPath .= '.'.$variantCreatorCommand['imageVariantExtension'];
713
                        // Delete the variant file in case it is stale
714
                        try {
715
                            $volume->deleteFile($variantPath);
716
                        } catch (VolumeException $e) {
717
                            // We're fine with that.
718
                        }
719
                        Craft::info(
720
                            'Deleted variant: '.$variantPath,
721
                            __METHOD__
722
                        );
723
                    }
724
                }
725
            }
726
        }
727
    }
728
729
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Parameter $variantCreatorCommand should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $outputPath should have a doc-comment as per coding-style.
Loading history...
730
     * @param                     $variantCreatorCommand
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 21
Loading history...
731
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 17 spaces after parameter type; 15 found
Loading history...
732
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
733
     * @param                     $outputPath
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 21
Loading history...
734
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
735
    protected function copyImageVariantToVolume(
736
        $variantCreatorCommand,
737
        Asset $asset,
738
        AssetTransformIndex $index,
739
        $outputPath
740
    ) {
741
        // If the image variant creation succeeded, copy it into place
742
        if (!empty($outputPath) && is_file($outputPath)) {
743
            // Figure out the resulting path for the image variant
744
            try {
745
                $volume = $asset->getVolume();
746
            } catch (InvalidConfigException $e) {
747
                $volume = null;
748
                Craft::error(
749
                    'Asset volume error: '.$e->getMessage(),
750
                    __METHOD__
751
                );
752
            }
753
            $assetTransforms = Craft::$app->getAssetTransforms();
754
            try {
755
                $transformPath = $asset->getFolder()->path.$assetTransforms->getTransformSubpath($asset, $index);
756
            } catch (InvalidConfigException $e) {
757
                $transformPath = '';
758
                Craft::error(
759
                    'Error getting asset folder: '.$e->getMessage(),
760
                    __METHOD__
761
                );
762
            }
763
            $variantPath = $transformPath.'.'.$variantCreatorCommand['imageVariantExtension'];
764
765
            // Delete the variant file in case it is stale
766
            try {
767
                $volume->deleteFile($variantPath);
768
            } catch (VolumeException $e) {
769
                // We're fine with that.
770
            }
771
772
            Craft::info(
773
                'Variant output path: '.$outputPath.' - Variant path: '.$variantPath,
774
                __METHOD__
775
            );
776
777
            clearstatcache(true, $outputPath);
778
            $stream = @fopen($outputPath, 'rb');
779
            if ($stream !== false) {
780
                // Now create it
781
                try {
782
                    $volume->createFileByStream($variantPath, $stream, []);
783
                } catch (VolumeException $e) {
784
                    Craft::error(
785
                        Craft::t('image-optimize', 'Failed to create image variant at: ')
786
                        .$outputPath,
787
                        __METHOD__
788
                    );
789
                }
790
791
                FileHelper::unlink($outputPath);
792
            }
793
        } else {
794
            Craft::error(
795
                Craft::t('image-optimize', 'Failed to create image variant at: ')
796
                .$outputPath,
797
                __METHOD__
798
            );
799
        }
800
    }
801
802
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
803
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
804
     * @param string $extension
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
805
     *
806
     * @return string
807
     */
808
    protected function swapPathExtension(string $path, string $extension): string
809
    {
810
        $pathParts = pathinfo($path);
811
        $newPath = $pathParts['filename'].'.'.$extension;
812
        if (!empty($pathParts['dirname']) && $pathParts['dirname'] !== '.') {
813
            $newPath = $pathParts['dirname'].DIRECTORY_SEPARATOR.$newPath;
814
            $newPath = preg_replace('#/+#', '/', $newPath);
815
        }
816
817
        return $newPath;
818
    }
819
}
820