Passed
Push — develop ( 02dcc0...b4966a )
by Andrew
66:22 queued 61:23
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\helpers\PluginTemplate as PluginTemplateHelper;
15
use nystudio107\imageoptimize\imagetransforms\CraftImageTransform;
16
use nystudio107\imageoptimize\imagetransforms\ImageTransform;
17
use nystudio107\imageoptimize\imagetransforms\ImageTransformInterface;
18
use nystudio107\imageoptimizeimgix\imagetransforms\ImgixImageTransform;
19
use nystudio107\imageoptimizethumbor\imagetransforms\ThumborImageTransform;
20
use nystudio107\imageoptimizesharp\imagetransforms\SharpImageTransform;
21
22
use Craft;
23
use craft\base\Component;
24
use craft\base\Image;
25
use craft\elements\Asset;
26
use craft\errors\ImageException;
27
use craft\errors\VolumeException;
28
use craft\events\AssetTransformImageEvent;
29
use craft\events\GetAssetThumbUrlEvent;
30
use craft\events\GetAssetUrlEvent;
31
use craft\events\GenerateTransformEvent;
32
use craft\events\RegisterComponentTypesEvent;
33
use craft\helpers\Assets as AssetsHelper;
34
use craft\helpers\Component as ComponentHelper;
35
use craft\helpers\FileHelper;
36
use craft\helpers\Html;
37
use craft\helpers\Image as ImageHelper;
38
use craft\image\Raster;
39
use craft\models\AssetTransform;
40
use craft\models\AssetTransformIndex;
41
42
use mikehaertl\shellcommand\Command as ShellCommand;
43
44
use yii\base\InvalidConfigException;
45
46
/** @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...
47
48
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
49
 * @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...
50
 * @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...
51
 * @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...
52
 */
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...
53
class Optimize extends Component
54
{
55
    // Constants
56
    // =========================================================================
57
58
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
59
     * @event RegisterComponentTypesEvent The event that is triggered when registering
60
     *        Image Transform types
61
     *
62
     * Image Transform types must implement [[ImageTransformInterface]]. [[ImageTransform]]
63
     * provides a base implementation.
64
     *
65
     * ```php
66
     * use nystudio107\imageoptimize\services\Optimize;
67
     * use craft\events\RegisterComponentTypesEvent;
68
     * use yii\base\Event;
69
     *
70
     * Event::on(Optimize::class,
71
     *     Optimize::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES,
72
     *     function(RegisterComponentTypesEvent $event) {
73
     *         $event->types[] = MyImageTransform::class;
74
     *     }
75
     * );
76
     * ```
77
     */
78
    const EVENT_REGISTER_IMAGE_TRANSFORM_TYPES = 'registerImageTransformTypes';
79
80
    const DEFAULT_IMAGE_TRANSFORM_TYPES = [
81
        CraftImageTransform::class,
82
        ImgixImageTransform::class,
83
        SharpImageTransform::class,
84
        ThumborImageTransform::class,
85
    ];
86
87
    // Public Methods
88
    // =========================================================================
89
90
    /**
91
     * Returns all available field type classes.
92
     *
93
     * @return string[] The available field type classes
94
     */
95
    public function getAllImageTransformTypes(): array
96
    {
97
        $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...
98
            ImageOptimize::$plugin->getSettings()->defaultImageTransformTypes ?? [],
99
            self::DEFAULT_IMAGE_TRANSFORM_TYPES
100
        ), 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...
101
102
        $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...
103
            'types' => $imageTransformTypes
104
        ]);
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...
105
        $this->trigger(self::EVENT_REGISTER_IMAGE_TRANSFORM_TYPES, $event);
106
107
        return $event->types;
108
    }
109
110
    /**
111
     * Creates an Image Transform with a given config.
112
     *
113
     * @param mixed $config The Image Transform’s class name, or its config,
114
     *                      with a `type` value and optionally a `settings` value
115
     *
116
     * @return null|ImageTransformInterface The Image Transform
117
     */
118
    public function createImageTransformType($config): ImageTransformInterface
119
    {
120
        if (is_string($config)) {
121
            $config = ['type' => $config];
122
        }
123
124
        try {
125
            /** @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...
126
            $imageTransform = ComponentHelper::createComponent($config, ImageTransformInterface::class);
127
        } catch (\Throwable $e) {
128
            $imageTransform = null;
129
            Craft::error($e->getMessage(), __METHOD__);
130
        }
131
132
        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...
133
    }
134
135
    /**
136
     * Handle responding to EVENT_GET_ASSET_URL events
137
     *
138
     * @param GetAssetUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
139
     *
140
     * @return null|string
141
     * @throws InvalidConfigException
142
     */
143
    public function handleGetAssetUrlEvent(GetAssetUrlEvent $event)
144
    {
145
        Craft::beginProfile('handleGetAssetUrlEvent', __METHOD__);
146
        $url = null;
147
        if (!ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform) {
148
            $asset = $event->asset;
149
            $transform = $event->transform;
150
            // If the transform is empty in some regard, normalize it to null
151
            if (empty($transform)) {
152
                $transform = null;
153
            }
154
            // If there's no transform requested, and we can't manipulate the image anyway, just return the URL
155
            if ($transform === null
156
                && !ImageHelper::canManipulateAsImage(pathinfo($asset->filename, PATHINFO_EXTENSION))) {
0 ignored issues
show
Bug introduced by
It seems like $asset->filename can also be of type null; however, parameter $path of pathinfo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
                && !ImageHelper::canManipulateAsImage(pathinfo(/** @scrutinizer ignore-type */ $asset->filename, PATHINFO_EXTENSION))) {
Loading history...
Bug introduced by
It seems like pathinfo($asset->filenam...ces\PATHINFO_EXTENSION) can also be of type array; however, parameter $extension of craft\helpers\Image::canManipulateAsImage() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
                && !ImageHelper::canManipulateAsImage(/** @scrutinizer ignore-type */ pathinfo($asset->filename, PATHINFO_EXTENSION))) {
Loading history...
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
157
                $volume = $asset->getVolume();
158
159
                return AssetsHelper::generateUrl($volume, $asset);
160
            }
161
            // If we're passed in null, make a dummy AssetTransform model for Thumbor
162
            // For backwards compatibility
163
            if ($transform === null && ImageOptimize::$plugin->transformMethod instanceof ThumborImageTransform) {
164
                $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...
165
                    'width'     => $asset->width,
166
                    'interlace' => 'line',
167
                ]);
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...
168
            }
169
            // If we're passed an array, make an AssetTransform model out of it
170
            if (\is_array($transform)) {
171
                $transform = new AssetTransform($transform);
172
            }
173
            // If we're passing in a string, look up the asset transform in the db
174
            if (\is_string($transform)) {
175
                $assetTransforms = Craft::$app->getAssetTransforms();
176
                $transform = $assetTransforms->getTransformByHandle($transform);
177
            }
178
            // If the final format is an SVG, don't attempt to transform it
179
            $finalFormat = empty($transform['format']) ? $asset->getExtension() : $transform['format'];
180
            if ($finalFormat === 'svg') {
181
                return null;
182
            }
183
            // Normalize the extension to lowercase, for some transform methods that require this
184
            if (!empty($transform) && !empty($finalFormat)) {
185
                $format = $transform['format'] ?? null;
186
                $transform['format'] = $format === null ? null : strtolower($finalFormat);
187
            }
188
            // Generate an image transform url
189
            $url = ImageOptimize::$plugin->transformMethod->getTransformUrl(
190
                $asset,
191
                $transform
192
            );
193
        }
194
        Craft::endProfile('handleGetAssetUrlEvent', __METHOD__);
195
196
        return $url;
197
    }
198
199
    /**
200
     * Handle responding to EVENT_GET_ASSET_THUMB_URL events
201
     *
202
     * @param GetAssetThumbUrlEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
203
     *
204
     * @return null|string
205
     */
206
    public function handleGetAssetThumbUrlEvent(GetAssetThumbUrlEvent $event)
207
    {
208
        Craft::beginProfile('handleGetAssetThumbUrlEvent', __METHOD__);
209
        $url = $event->url;
210
        if (!ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform) {
211
            $asset = $event->asset;
212
            if (ImageHelper::canManipulateAsImage($asset->getExtension())) {
213
                $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...
214
                    'width' => $event->width,
215
		    '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...
216
                    'interlace' => 'line',
217
                ]);
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...
218
                /** @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...
219
                $transformMethod = ImageOptimize::$plugin->transformMethod;
220
                // If the final format is an SVG, don't attempt to transform it
221
                $finalFormat = empty($transform['format']) ? $asset->getExtension() : $transform['format'];
222
                if ($finalFormat === 'svg') {
223
                    return null;
224
                }
225
                // Normalize the extension to lowercase, for some transform methods that require this
226
                if ($transform !== null && !empty($finalFormat)) {
227
                    $transform['format'] = strtolower($finalFormat);
228
                }
229
                // Generate an image transform url
230
                if ($transformMethod->hasProperty('generateTransformsBeforePageLoad')) {
231
                    $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...
232
                }
233
                $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...
234
            }
235
        }
236
        Craft::endProfile('handleGetAssetThumbUrlEvent', __METHOD__);
237
238
        return $url;
239
    }
240
241
    /**
242
     * Render the lazy load JavaScript shim
243
     *
244
     * @param array $scriptAttrs
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 2 spaces but found 1
Loading history...
245
     * @param array $variables
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 2 spaces but found 1
Loading history...
246
     * @return string
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
247
     */
248
    public function renderLazyLoadJs($scriptAttrs = [], $variables = [])
249
    {
250
        $minifier = 'minify';
251
        if ($scriptAttrs === null) {
0 ignored issues
show
introduced by
The condition $scriptAttrs === null is always false.
Loading history...
252
            $minifier = 'jsMin';
253
        }
254
        $vars = 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...
255
            'scriptSrc' => 'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.0/lazysizes.min.js',
256
            ],
257
            $variables,
258
        );
259
        $content = PluginTemplateHelper::renderPluginTemplate(
260
            'frontend/lazyload-image-shim',
261
            $vars,
262
            $minifier
263
        );
264
        $content = (string)$content;
265
        if ($scriptAttrs !== null) {
0 ignored issues
show
introduced by
The condition $scriptAttrs !== null is always true.
Loading history...
266
            $attrs = array_merge([
0 ignored issues
show
Unused Code introduced by
The assignment to $attrs is dead and can be removed.
Loading history...
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
267
                ],
268
                $scriptAttrs,
269
            );
270
            $content = Html::tag('script', $content, $scriptAttrs);
271
        }
272
273
        return $content;
274
    }
275
276
    /**
277
     * Handle responding to EVENT_GENERATE_TRANSFORM events
278
     *
279
     * @param GenerateTransformEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
280
     *
281
     * @return null|string
282
     */
283
    public function handleGenerateTransformEvent(GenerateTransformEvent $event)
284
    {
285
        Craft::beginProfile('handleGenerateTransformEvent', __METHOD__);
286
        $tempPath = null;
287
288
        // Only do this for local Craft transforms
289
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
290
            // Apply any filters to the image
291
            if ($event->transformIndex->transform !== null) {
292
                $this->applyFiltersToImage($event->transformIndex->transform, $event->asset, $event->image);
293
            }
294
            // Save the transformed image to a temp file
295
            $tempPath = $this->saveTransformToTempFile(
296
                $event->transformIndex,
297
                $event->image
298
            );
299
            $originalFileSize = @filesize($tempPath);
300
            // Optimize the image
301
            $this->optimizeImage(
302
                $event->transformIndex,
303
                $tempPath
304
            );
305
            clearstatcache(true, $tempPath);
306
            // Log the results of the image optimization
307
            $optimizedFileSize = @filesize($tempPath);
308
            $index = $event->transformIndex;
309
            Craft::info(
310
                pathinfo($index->filename, PATHINFO_FILENAME)
0 ignored issues
show
Bug introduced by
It seems like $index->filename can also be of type null; however, parameter $path of pathinfo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
                pathinfo(/** @scrutinizer ignore-type */ $index->filename, PATHINFO_FILENAME)
Loading history...
Bug introduced by
Are you sure pathinfo($index->filenam...ices\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
                /** @scrutinizer ignore-type */ pathinfo($index->filename, PATHINFO_FILENAME)
Loading history...
311
                .'.'
312
                .$index->detectedFormat
313
                .' -> '
314
                .Craft::t('image-optimize', 'Original')
315
                .': '
316
                .$this->humanFileSize($originalFileSize, 1)
317
                .', '
318
                .Craft::t('image-optimize', 'Optimized')
319
                .': '
320
                .$this->humanFileSize($optimizedFileSize, 1)
321
                .' -> '
322
                .Craft::t('image-optimize', 'Savings')
323
                .': '
324
                .number_format(abs(100 - (($optimizedFileSize * 100) / $originalFileSize)), 1)
325
                .'%',
326
                __METHOD__
327
            );
328
            // Create any image variants
329
            $this->createImageVariants(
330
                $event->transformIndex,
331
                $event->asset,
332
                $tempPath
333
            );
334
        }
335
        Craft::endProfile('handleGenerateTransformEvent', __METHOD__);
336
337
        return $tempPath;
338
    }
339
340
    /**
341
     * Handle cleaning up any variant creator images
342
     *
343
     * @param AssetTransformImageEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
344
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
345
    public function handleAfterDeleteTransformsEvent(AssetTransformImageEvent $event)
346
    {
347
        $settings = ImageOptimize::$plugin->getSettings();
0 ignored issues
show
Unused Code introduced by
The assignment to $settings is dead and can be removed.
Loading history...
348
        // Only do this for local Craft transforms
349
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
350
            $this->cleanupImageVariants($event->asset, $event->transformIndex);
351
        }
352
    }
353
354
    /**
355
     * Save out the image to a temp file
356
     *
357
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
358
     * @param Image               $image
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
359
     *
360
     * @return string
361
     */
362
    public function saveTransformToTempFile(AssetTransformIndex $index, Image $image): string
363
    {
364
        $tempFilename = uniqid(pathinfo($index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
0 ignored issues
show
Bug introduced by
It seems like $index->filename can also be of type null; however, parameter $path of pathinfo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

364
        $tempFilename = uniqid(pathinfo(/** @scrutinizer ignore-type */ $index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
Loading history...
Bug introduced by
It seems like pathinfo($index->filenam...ices\PATHINFO_FILENAME) can also be of type array; however, parameter $prefix of uniqid() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

364
        $tempFilename = uniqid(/** @scrutinizer ignore-type */ pathinfo($index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
Loading history...
365
        $tempPath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$tempFilename;
366
        try {
367
            $image->saveAs($tempPath);
368
        } catch (ImageException $e) {
369
            Craft::error('Transformed image save failed: '.$e->getMessage(), __METHOD__);
370
        }
371
        Craft::info('Transformed image saved to: '.$tempPath, __METHOD__);
372
373
        return $tempPath;
374
    }
375
376
    /**
377
     * Run any image post-processing/optimization on the image file
378
     *
379
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
380
     * @param string              $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
381
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
382
    public function optimizeImage(AssetTransformIndex $index, string $tempPath)
383
    {
384
        Craft::beginProfile('optimizeImage', __METHOD__);
385
        $settings = ImageOptimize::$plugin->getSettings();
386
        // Get the active processors for the transform format
387
        $activeImageProcessors = $settings->activeImageProcessors;
388
        $fileFormat = $index->detectedFormat;
389
        // Special-case for 'jpeg'
390
        if ($fileFormat === 'jpeg') {
391
            $fileFormat = 'jpg';
392
        }
393
        if (!empty($activeImageProcessors[$fileFormat])) {
394
            // Iterate through all of the processors for this format
395
            $imageProcessors = $settings->imageProcessors;
396
            foreach ($activeImageProcessors[$fileFormat] as $processor) {
397
                if (!empty($processor) && !empty($imageProcessors[$processor])) {
398
                    $this->executeImageProcessor($imageProcessors[$processor], $tempPath);
399
                }
400
            }
401
        }
402
        Craft::endProfile('optimizeImage', __METHOD__);
403
    }
404
405
    /**
406
     * Translate bytes into something human-readable
407
     *
408
     * @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...
409
     * @param int $decimals
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
410
     *
411
     * @return string
412
     */
413
    public function humanFileSize($bytes, $decimals = 1): string
414
    {
415
        $oldSize = Craft::$app->formatter->sizeFormatBase;
416
        Craft::$app->formatter->sizeFormatBase = 1000;
417
        $result = Craft::$app->formatter->asShortSize($bytes, $decimals);
418
        Craft::$app->formatter->sizeFormatBase = $oldSize;
419
420
        return $result;
421
    }
422
423
    /**
424
     * Create any image variants for the image file
425
     *
426
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
427
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
428
     * @param string              $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
429
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
430
    public function createImageVariants(AssetTransformIndex $index, Asset $asset, string $tempPath)
431
    {
432
        Craft::beginProfile('createImageVariants', __METHOD__);
433
        $settings = ImageOptimize::$plugin->getSettings();
434
        // Get the active image variant creators
435
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
436
        $fileFormat = $index->detectedFormat ?? $index->format;
437
        // Special-case for 'jpeg'
438
        if ($fileFormat === 'jpeg') {
439
            $fileFormat = 'jpg';
440
        }
441
        if (!empty($activeImageVariantCreators[$fileFormat])) {
442
            // Iterate through all of the image variant creators for this format
443
            $imageVariantCreators = $settings->imageVariantCreators;
444
            foreach ($activeImageVariantCreators[$fileFormat] as $variantCreator) {
445
                if (!empty($variantCreator) && !empty($imageVariantCreators[$variantCreator])) {
446
                    // Create the image variant in a temporary folder
447
                    $generalConfig = Craft::$app->getConfig()->getGeneral();
448
                    $quality = $index->transform->quality ?: $generalConfig->defaultImageQuality;
449
                    $outputPath = $this->executeVariantCreator(
450
                        $imageVariantCreators[$variantCreator],
451
                        $tempPath,
452
                        $quality
453
                    );
454
455
                    if ($outputPath !== null) {
456
                        // Get info on the original and the created variant
457
                        $originalFileSize = @filesize($tempPath);
458
                        $variantFileSize = @filesize($outputPath);
459
460
                        Craft::info(
461
                            pathinfo($tempPath, PATHINFO_FILENAME)
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($tempPath, nyst...ices\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

461
                            /** @scrutinizer ignore-type */ pathinfo($tempPath, PATHINFO_FILENAME)
Loading history...
462
                            .'.'
463
                            .pathinfo($tempPath, PATHINFO_EXTENSION)
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($tempPath, nyst...ces\PATHINFO_EXTENSION) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

463
                            ./** @scrutinizer ignore-type */ pathinfo($tempPath, PATHINFO_EXTENSION)
Loading history...
464
                            .' -> '
465
                            .pathinfo($outputPath, PATHINFO_FILENAME)
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($outputPath, ny...ices\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

465
                            ./** @scrutinizer ignore-type */ pathinfo($outputPath, PATHINFO_FILENAME)
Loading history...
466
                            .'.'
467
                            .pathinfo($outputPath, PATHINFO_EXTENSION)
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($outputPath, ny...ces\PATHINFO_EXTENSION) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

467
                            ./** @scrutinizer ignore-type */ pathinfo($outputPath, PATHINFO_EXTENSION)
Loading history...
468
                            .' -> '
469
                            .Craft::t('image-optimize', 'Original')
470
                            .': '
471
                            .$this->humanFileSize($originalFileSize, 1)
472
                            .', '
473
                            .Craft::t('image-optimize', 'Variant')
474
                            .': '
475
                            .$this->humanFileSize($variantFileSize, 1)
476
                            .' -> '
477
                            .Craft::t('image-optimize', 'Savings')
478
                            .': '
479
                            .number_format(abs(100 - (($variantFileSize * 100) / $originalFileSize)), 1)
480
                            .'%',
481
                            __METHOD__
482
                        );
483
484
                        // Copy the image variant into place
485
                        $this->copyImageVariantToVolume(
486
                            $imageVariantCreators[$variantCreator],
487
                            $asset,
488
                            $index,
489
                            $outputPath
490
                        );
491
                    }
492
                }
493
            }
494
        }
495
        Craft::endProfile('createImageVariants', __METHOD__);
496
    }
497
498
    /**
499
     * Return an array of active image processors
500
     *
501
     * @return array
502
     */
503
    public function getActiveImageProcessors(): array
504
    {
505
        $result = [];
506
        $settings = ImageOptimize::$plugin->getSettings();
507
        // Get the active processors for the transform format
508
        $activeImageProcessors = $settings->activeImageProcessors;
509
        foreach ($activeImageProcessors as $imageFormat => $imageProcessor) {
510
            // Iterate through all of the processors for this format
511
            $imageProcessors = $settings->imageProcessors;
512
            foreach ($activeImageProcessors[$imageFormat] as $processor) {
513
                if (!empty($imageProcessors[$processor])) {
514
                    $thisImageProcessor = $imageProcessors[$processor];
515
                    $result[] = [
516
                        'format'    => $imageFormat,
517
                        'creator'   => $processor,
518
                        'command'   => $thisImageProcessor['commandPath']
519
                            .' '
520
                            .$thisImageProcessor['commandOptions'],
521
                        'installed' => is_file($thisImageProcessor['commandPath']),
522
                    ];
523
                }
524
            }
525
        }
526
527
        return $result;
528
    }
529
530
    /**
531
     * Return an array of active image variant creators
532
     *
533
     * @return array
534
     */
535
    public function getActiveVariantCreators(): array
536
    {
537
        $result = [];
538
        $settings = ImageOptimize::$plugin->getSettings();
539
        // Get the active image variant creators
540
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
541
        foreach ($activeImageVariantCreators as $imageFormat => $imageCreator) {
542
            // Iterate through all of the image variant creators for this format
543
            $imageVariantCreators = $settings->imageVariantCreators;
544
            foreach ($activeImageVariantCreators[$imageFormat] as $variantCreator) {
545
                if (!empty($imageVariantCreators[$variantCreator])) {
546
                    $thisVariantCreator = $imageVariantCreators[$variantCreator];
547
                    $result[] = [
548
                        'format'    => $imageFormat,
549
                        'creator'   => $variantCreator,
550
                        'command'   => $thisVariantCreator['commandPath']
551
                            .' '
552
                            .$thisVariantCreator['commandOptions'],
553
                        'installed' => is_file($thisVariantCreator['commandPath']),
554
                    ];
555
                }
556
            }
557
        }
558
559
        return $result;
560
    }
561
562
    // Protected Methods
563
    // =========================================================================
564
565
    /** @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...
566
     * @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...
567
     * @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...
568
     * @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...
569
     */
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...
570
571
    protected function applyFiltersToImage(AssetTransform $transform, Asset $asset, Image $image)
572
    {
573
        $settings = ImageOptimize::$plugin->getSettings();
574
        // Only try to apply filters to Raster images
575
        if ($image instanceof Raster) {
576
            $imagineImage = $image->getImagineImage();
577
            if ($imagineImage !== null) {
578
                // Handle auto-sharpening scaled down images
579
                if ($settings->autoSharpenScaledImages) {
580
                    // See if the image has been scaled >= 50%
581
                    $widthScale = $asset->getWidth() / $image->getWidth();
582
                    $heightScale = $asset->getHeight() / $image->getHeight();
583
                    if (($widthScale >= 2.0) || ($heightScale >= 2.0)) {
584
                        $imagineImage->effects()
585
                            ->sharpen();
586
                        Craft::debug(
587
                            Craft::t(
588
                                'image-optimize',
589
                                'Image transform >= 50%, sharpened the transformed image: {name}',
590
                                [
591
                                    'name' => $asset->title,
592
                                ]
593
                            ),
594
                            __METHOD__
595
                        );
596
                    }
597
                }
598
            }
599
        }
600
    }
601
602
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
603
     * @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...
604
     * @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...
605
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
606
    protected function executeImageProcessor($thisProcessor, string $tempPath)
607
    {
608
        // Make sure the command exists
609
        if (is_file($thisProcessor['commandPath'])) {
610
            // Set any options for the command
611
            $commandOptions = '';
612
            if (!empty($thisProcessor['commandOptions'])) {
613
                $commandOptions = ' '
614
                    .$thisProcessor['commandOptions']
615
                    .' ';
616
            }
617
            // Redirect the command output if necessary for this processor
618
            $outputFileFlag = '';
619
            if (!empty($thisProcessor['commandOutputFileFlag'])) {
620
                $outputFileFlag = ' '
621
                    .$thisProcessor['commandOutputFileFlag']
622
                    .' '
623
                    .escapeshellarg($tempPath)
624
                    .' ';
625
            }
626
            // Build the command to execute
627
            $cmd =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
628
                $thisProcessor['commandPath']
629
                .$commandOptions
630
                .$outputFileFlag
631
                .escapeshellarg($tempPath);
632
            // Execute the command
633
            $shellOutput = $this->executeShellCommand($cmd);
634
            Craft::info($cmd."\n".$shellOutput, __METHOD__);
635
        } else {
636
            Craft::error(
637
                $thisProcessor['commandPath']
638
                .' '
639
                .Craft::t('image-optimize', 'does not exist'),
640
                __METHOD__
641
            );
642
        }
643
    }
644
645
    /**
646
     * Execute a shell command
647
     *
648
     * @param string $command
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
649
     *
650
     * @return string
651
     */
652
    protected function executeShellCommand(string $command): string
653
    {
654
        // Create the shell command
655
        $shellCommand = new ShellCommand();
656
        $shellCommand->setCommand($command);
657
658
        // If we don't have proc_open, maybe we've got exec
659
        if (!\function_exists('proc_open') && \function_exists('exec')) {
660
            $shellCommand->useExec = true;
661
        }
662
663
        // Return the result of the command's output or error
664
        if ($shellCommand->execute()) {
665
            $result = $shellCommand->getOutput();
666
        } else {
667
            $result = $shellCommand->getError();
668
        }
669
670
        return $result;
671
    }
672
673
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
674
     * @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...
675
     * @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...
676
     * @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...
677
     *
678
     * @return string|null the path to the created variant
679
     */
680
    protected function executeVariantCreator($variantCreatorCommand, string $tempPath, int $imageQuality)
681
    {
682
        $outputPath = $tempPath;
683
        // Make sure the command exists
684
        if (is_file($variantCreatorCommand['commandPath'])) {
685
            // Get the output file for this image variant
686
            $outputPath .= '.'.$variantCreatorCommand['imageVariantExtension'];
687
            // Set any options for the command
688
            $commandOptions = '';
689
            if (!empty($variantCreatorCommand['commandOptions'])) {
690
                $commandOptions = ' '
691
                    .$variantCreatorCommand['commandOptions']
692
                    .' ';
693
            }
694
            // Redirect the command output if necessary for this variantCreator
695
            $outputFileFlag = '';
696
            if (!empty($variantCreatorCommand['commandOutputFileFlag'])) {
697
                $outputFileFlag = ' '
698
                    .$variantCreatorCommand['commandOutputFileFlag']
699
                    .' '
700
                    .escapeshellarg($outputPath)
701
                    .' ';
702
            }
703
            // Get the quality setting of this transform
704
            $commandQualityFlag = '';
705
            if (!empty($variantCreatorCommand['commandQualityFlag'])) {
706
                $commandQualityFlag = ' '
707
                    .$variantCreatorCommand['commandQualityFlag']
708
                    .' '
709
                    .$imageQuality
710
                    .' ';
711
            }
712
            // Build the command to execute
713
            $cmd =
0 ignored issues
show
Coding Style introduced by
Multi-line assignments must have the equal sign on the second line
Loading history...
714
                $variantCreatorCommand['commandPath']
715
                .$commandOptions
716
                .$commandQualityFlag
717
                .$outputFileFlag
718
                .escapeshellarg($tempPath);
719
            // Execute the command
720
            $shellOutput = $this->executeShellCommand($cmd);
721
            Craft::info($cmd."\n".$shellOutput, __METHOD__);
722
        } else {
723
            Craft::error(
724
                $variantCreatorCommand['commandPath']
725
                .' '
726
                .Craft::t('image-optimize', 'does not exist'),
727
                __METHOD__
728
            );
729
            $outputPath = null;
730
        }
731
732
        return $outputPath;
733
    }
734
735
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
736
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
737
     * @param AssetTransformIndex $transformIndex
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
738
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
739
    protected function cleanupImageVariants(Asset $asset, AssetTransformIndex $transformIndex)
740
    {
741
        $settings = ImageOptimize::$plugin->getSettings();
742
        $assetTransforms = Craft::$app->getAssetTransforms();
743
        // Get the active image variant creators
744
        $activeImageVariantCreators = $settings->activeImageVariantCreators;
745
        $fileFormat = $transformIndex->detectedFormat ?? $transformIndex->format;
746
        if (!empty($activeImageVariantCreators[$fileFormat])) {
747
            // Iterate through all of the image variant creators for this format
748
            $imageVariantCreators = $settings->imageVariantCreators;
749
            if (!empty($activeImageVariantCreators[$fileFormat])) {
750
                foreach ($activeImageVariantCreators[$fileFormat] as $variantCreator) {
751
                    if (!empty($variantCreator) && !empty($imageVariantCreators[$variantCreator])) {
752
                        // Create the image variant in a temporary folder
753
                        $variantCreatorCommand = $imageVariantCreators[$variantCreator];
754
                        try {
755
                            $volume = $asset->getVolume();
756
                        } catch (InvalidConfigException $e) {
757
                            $volume = null;
758
                            Craft::error(
759
                                'Asset volume error: '.$e->getMessage(),
760
                                __METHOD__
761
                            );
762
                        }
763
                        try {
764
                            $variantPath = $asset->getFolder()->path.$assetTransforms->getTransformSubpath(
765
                                $asset,
766
                                $transformIndex
767
                            );
768
                        } catch (InvalidConfigException $e) {
769
                            $variantPath = '';
770
                            Craft::error(
771
                                'Asset folder does not exist: '.$e->getMessage(),
772
                                __METHOD__
773
                            );
774
                        }
775
                        $variantPath .= '.'.$variantCreatorCommand['imageVariantExtension'];
776
                        // Delete the variant file in case it is stale
777
                        try {
778
                            $volume->deleteFile($variantPath);
779
                        } catch (VolumeException $e) {
780
                            // We're fine with that.
781
                        }
782
                        Craft::info(
783
                            'Deleted variant: '.$variantPath,
784
                            __METHOD__
785
                        );
786
                    }
787
                }
788
            }
789
        }
790
    }
791
792
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
793
     * @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...
794
     * @param Asset               $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
795
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
796
     * @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...
797
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
798
    protected function copyImageVariantToVolume(
799
        $variantCreatorCommand,
800
        Asset $asset,
801
        AssetTransformIndex $index,
802
        $outputPath
803
    ) {
804
        // If the image variant creation succeeded, copy it into place
805
        if (!empty($outputPath) && is_file($outputPath)) {
806
            // Figure out the resulting path for the image variant
807
            try {
808
                $volume = $asset->getVolume();
809
            } catch (InvalidConfigException $e) {
810
                $volume = null;
811
                Craft::error(
812
                    'Asset volume error: '.$e->getMessage(),
813
                    __METHOD__
814
                );
815
            }
816
            $assetTransforms = Craft::$app->getAssetTransforms();
817
            try {
818
                $transformPath = $asset->getFolder()->path.$assetTransforms->getTransformSubpath($asset, $index);
819
            } catch (InvalidConfigException $e) {
820
                $transformPath = '';
821
                Craft::error(
822
                    'Error getting asset folder: '.$e->getMessage(),
823
                    __METHOD__
824
                );
825
            }
826
            $variantPath = $transformPath.'.'.$variantCreatorCommand['imageVariantExtension'];
827
828
            // Delete the variant file in case it is stale
829
            try {
830
                $volume->deleteFile($variantPath);
831
            } catch (VolumeException $e) {
832
                // We're fine with that.
833
            }
834
835
            Craft::info(
836
                'Variant output path: '.$outputPath.' - Variant path: '.$variantPath,
837
                __METHOD__
838
            );
839
840
            clearstatcache(true, $outputPath);
841
            $stream = @fopen($outputPath, 'rb');
842
            if ($stream !== false) {
843
                // Now create it
844
                try {
845
                    $volume->createFileByStream($variantPath, $stream, []);
846
                } catch (VolumeException $e) {
847
                    Craft::error(
848
                        Craft::t('image-optimize', 'Failed to create image variant at: ')
849
                        .$outputPath,
850
                        __METHOD__
851
                    );
852
                }
853
854
                FileHelper::unlink($outputPath);
855
            }
856
        } else {
857
            Craft::error(
858
                Craft::t('image-optimize', 'Failed to create image variant at: ')
859
                .$outputPath,
860
                __METHOD__
861
            );
862
        }
863
    }
864
865
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
866
     * @param string $path
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
867
     * @param string $extension
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
868
     *
869
     * @return string
870
     */
871
    protected function swapPathExtension(string $path, string $extension): string
872
    {
873
        $pathParts = pathinfo($path);
874
        $newPath = $pathParts['filename'].'.'.$extension;
875
        if (!empty($pathParts['dirname']) && $pathParts['dirname'] !== '.') {
876
            $newPath = $pathParts['dirname'].DIRECTORY_SEPARATOR.$newPath;
877
            $newPath = preg_replace('#/+#', '/', $newPath);
878
        }
879
880
        return $newPath;
881
    }
882
}
883