Passed
Push — develop ( d467c7...cee2dc )
by Andrew
03:47
created

Optimize::humanFileSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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