Passed
Push — develop ( 399d1e...2d2a1d )
by Andrew
06:19 queued 36s
created

Optimize::getActiveVariantCreators()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 25
rs 9.7333
cc 4
nc 4
nop 0
1
<?php
2
/**
3
 * ImageOptimize plugin for Craft CMS 3.x
4
 *
5
 * Automatically optimize images after they've been transformed
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\imageoptimize\services;
12
13
use nystudio107\imageoptimize\ImageOptimize;
14
use nystudio107\imageoptimize\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
     * Returns whether `.webp` is a format supported by the server
243
     *
244
     * @return bool
245
     */
246
    public function serverSupportsWebP(): bool
247
    {
248
        $result = false;
249
        $variantCreators = ImageOptimize::$plugin->optimize->getActiveVariantCreators();
250
        foreach ($variantCreators as $variantCreator) {
251
            if ($variantCreator['creator'] === 'cwebp' && $variantCreator['installed']) {
252
                $result = true;
253
            }
254
        }
255
256
        return $result;
257
    }
258
259
    /**
260
     * Render the lazy load JavaScript shim
261
     *
262
     * @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...
263
     * @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...
264
     * @return string
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
265
     */
266
    public function renderLazyLoadJs($scriptAttrs = [], $variables = [])
267
    {
268
        $minifier = 'minify';
269
        if ($scriptAttrs === null) {
0 ignored issues
show
introduced by
The condition $scriptAttrs === null is always false.
Loading history...
270
            $minifier = 'jsMin';
271
        }
272
        $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...
273
            'scriptSrc' => 'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.0/lazysizes.min.js',
274
            ],
275
            $variables,
276
        );
277
        $content = PluginTemplateHelper::renderPluginTemplate(
278
            'frontend/lazyload-image-shim',
279
            $vars,
280
            $minifier
281
        );
282
        $content = (string)$content;
283
        if ($scriptAttrs !== null) {
0 ignored issues
show
introduced by
The condition $scriptAttrs !== null is always true.
Loading history...
284
            $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...
285
                ],
286
                $scriptAttrs,
287
            );
288
            $content = Html::tag('script', $content, $scriptAttrs);
289
        }
290
291
        return $content;
292
    }
293
294
    /**
295
     * Handle responding to EVENT_GENERATE_TRANSFORM events
296
     *
297
     * @param GenerateTransformEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
298
     *
299
     * @return null|string
300
     */
301
    public function handleGenerateTransformEvent(GenerateTransformEvent $event)
302
    {
303
        Craft::beginProfile('handleGenerateTransformEvent', __METHOD__);
304
        $tempPath = null;
305
306
        // Only do this for local Craft transforms
307
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
308
            // Apply any filters to the image
309
            if ($event->transformIndex->transform !== null) {
310
                $this->applyFiltersToImage($event->transformIndex->transform, $event->asset, $event->image);
311
            }
312
            // Save the transformed image to a temp file
313
            $tempPath = $this->saveTransformToTempFile(
314
                $event->transformIndex,
315
                $event->image
316
            );
317
            $originalFileSize = @filesize($tempPath);
318
            // Optimize the image
319
            $this->optimizeImage(
320
                $event->transformIndex,
321
                $tempPath
322
            );
323
            clearstatcache(true, $tempPath);
324
            // Log the results of the image optimization
325
            $optimizedFileSize = @filesize($tempPath);
326
            $index = $event->transformIndex;
327
            Craft::info(
328
                pathinfo($index->filename, PATHINFO_FILENAME)
0 ignored issues
show
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

328
                /** @scrutinizer ignore-type */ pathinfo($index->filename, PATHINFO_FILENAME)
Loading history...
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

328
                pathinfo(/** @scrutinizer ignore-type */ $index->filename, PATHINFO_FILENAME)
Loading history...
329
                .'.'
330
                .$index->detectedFormat
331
                .' -> '
332
                .Craft::t('image-optimize', 'Original')
333
                .': '
334
                .$this->humanFileSize($originalFileSize, 1)
335
                .', '
336
                .Craft::t('image-optimize', 'Optimized')
337
                .': '
338
                .$this->humanFileSize($optimizedFileSize, 1)
339
                .' -> '
340
                .Craft::t('image-optimize', 'Savings')
341
                .': '
342
                .number_format(abs(100 - (($optimizedFileSize * 100) / $originalFileSize)), 1)
343
                .'%',
344
                __METHOD__
345
            );
346
            // Create any image variants
347
            $this->createImageVariants(
348
                $event->transformIndex,
349
                $event->asset,
350
                $tempPath
351
            );
352
        }
353
        Craft::endProfile('handleGenerateTransformEvent', __METHOD__);
354
355
        return $tempPath;
356
    }
357
358
    /**
359
     * Handle cleaning up any variant creator images
360
     *
361
     * @param AssetTransformImageEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
362
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
363
    public function handleAfterDeleteTransformsEvent(AssetTransformImageEvent $event)
364
    {
365
        $settings = ImageOptimize::$plugin->getSettings();
0 ignored issues
show
Unused Code introduced by
The assignment to $settings is dead and can be removed.
Loading history...
366
        // Only do this for local Craft transforms
367
        if (ImageOptimize::$plugin->transformMethod instanceof CraftImageTransform && $event->asset !== null) {
368
            $this->cleanupImageVariants($event->asset, $event->transformIndex);
369
        }
370
    }
371
372
    /**
373
     * Save out the image to a temp file
374
     *
375
     * @param AssetTransformIndex $index
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
376
     * @param Image               $image
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
377
     *
378
     * @return string
379
     */
380
    public function saveTransformToTempFile(AssetTransformIndex $index, Image $image): string
381
    {
382
        $tempFilename = uniqid(pathinfo($index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
0 ignored issues
show
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

382
        $tempFilename = uniqid(/** @scrutinizer ignore-type */ pathinfo($index->filename, PATHINFO_FILENAME), true).'.'.$index->detectedFormat;
Loading history...
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

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

479
                            /** @scrutinizer ignore-type */ pathinfo($tempPath, PATHINFO_FILENAME)
Loading history...
480
                            .'.'
481
                            .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

481
                            ./** @scrutinizer ignore-type */ pathinfo($tempPath, PATHINFO_EXTENSION)
Loading history...
482
                            .' -> '
483
                            .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

483
                            ./** @scrutinizer ignore-type */ pathinfo($outputPath, PATHINFO_FILENAME)
Loading history...
484
                            .'.'
485
                            .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

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