Passed
Push — develop ( f36603...4947b1 )
by Andrew
05:47
created

Optimize::humanFileSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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