Issues (1497)

src/services/Placeholder.php (62 issues)

1
<?php
2
/**
3
 * ImageOptimize plugin for Craft CMS
4
 *
5
 * Automatically optimize images after they've been transformed
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
PHP version not specified
Loading history...
Missing @category tag in file comment
Loading history...
Missing @package tag in file comment
Loading history...
Missing @author tag in file comment
Loading history...
Missing @license tag in file comment
Loading history...
10
11
namespace nystudio107\imageoptimize\services;
12
13
use ColorThief\ColorThief;
14
use Craft;
15
use craft\base\Component;
16
use craft\elements\Asset;
17
use craft\helpers\Image;
18
use craft\helpers\ImageTransforms as TransformHelper;
19
use craft\helpers\StringHelper;
20
use craft\image\Raster;
21
use Exception;
22
use nystudio107\imageoptimize\helpers\Color as ColorHelper;
23
use nystudio107\imageoptimize\ImageOptimize;
24
use nystudio107\imageoptimize\lib\Potracio;
25
use nystudio107\imageoptimize\models\Settings;
26
use Throwable;
27
use function function_exists;
28
use function strlen;
29
30
/**
0 ignored issues
show
Missing short description in doc comment
Loading history...
31
 * @author    nystudio107
0 ignored issues
show
The tag in position 1 should be the @package tag
Loading history...
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
32
 * @package   ImageOptimize
0 ignored issues
show
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
33
 * @since     1.0.0
0 ignored issues
show
The tag in position 3 should be the @author tag
Loading history...
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
34
 */
0 ignored issues
show
Missing @category tag in class comment
Loading history...
Missing @license tag in class comment
Loading history...
Missing @link tag in class comment
Loading history...
35
class Placeholder extends Component
36
{
37
    // Constants
38
    // =========================================================================
39
40
    protected const PLACEHOLDER_WIDTH = 16;
41
    protected const PLACEHOLDER_QUALITY = 50;
42
43
    protected const TEMP_PLACEHOLDER_WIDTH = 300;
44
    protected const TEMP_PLACEHOLDER_QUALITY = 75;
45
46
    protected const MAX_SILHOUETTE_SIZE = 30 * 1024;
47
48
    // Public Properties
49
    // =========================================================================
50
51
    // Public Methods
52
    // =========================================================================
53
54
    /**
55
     * Return an SVG box as a placeholder image
56
     *
57
     * @param             $width
0 ignored issues
show
Tag value for @param tag indented incorrectly; expected 1 spaces but found 13
Loading history...
Missing parameter comment
Loading history...
58
     * @param             $height
0 ignored issues
show
Missing parameter comment
Loading history...
Tag value for @param tag indented incorrectly; expected 1 spaces but found 13
Loading history...
59
     * @param ?string $color
0 ignored issues
show
Missing parameter comment
Loading history...
60
     *
61
     * @return string
62
     */
63
    public function generatePlaceholderBox($width, $height, ?string $color = null): string
64
    {
65
        $color = $color ?? '#CCC';
66
        $header = 'data:image/svg+xml,';
67
        $content = "<svg xmlns='http://www.w3.org/2000/svg' "
68
            . "width='$width' "
69
            . "height='$height' "
70
            . "style='background:$color' "
71
            . "/>";
72
73
        return $header . ImageOptimize::$plugin->optimizedImages->encodeOptimizedSVGDataUri($content);
74
    }
75
76
    /**
77
     * Generate a base64-encoded placeholder image
78
     *
79
     * @param string $tempPath
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 12 spaces after parameter type; 1 found
Loading history...
80
     * @param float $aspectRatio
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 13 spaces after parameter type; 1 found
Loading history...
81
     * @param mixed|string|null $position
0 ignored issues
show
Missing parameter comment
Loading history...
82
     *
83
     * @return string
84
     */
85
    public function generatePlaceholderImage(string $tempPath, float $aspectRatio, mixed $position): string
86
    {
87
        Craft::beginProfile('generatePlaceholderImage', __METHOD__);
88
        Craft::info(
89
            'Generating placeholder image for asset',
90
            __METHOD__
91
        );
92
        $result = '';
93
        $width = self::PLACEHOLDER_WIDTH;
94
        $height = (int)($width / $aspectRatio);
95
        $placeholderPath = $this->createImageFromPath($tempPath, $width, $height, self::PLACEHOLDER_QUALITY, $position);
96
        if (!empty($placeholderPath)) {
97
            $result = base64_encode(file_get_contents($placeholderPath));
98
            unlink($placeholderPath);
99
        }
100
        Craft::endProfile('generatePlaceholderImage', __METHOD__);
101
102
        return $result;
103
    }
104
105
    /**
106
     * Generate a color palette from the image
107
     *
108
     * @param string $tempPath
0 ignored issues
show
Missing parameter comment
Loading history...
109
     *
110
     * @return array
111
     */
112
    public function generateColorPalette(string $tempPath): array
113
    {
114
        Craft::beginProfile('generateColorPalette', __METHOD__);
115
        Craft::info(
116
            'Generating color palette for: ' . $tempPath,
117
            __METHOD__
118
        );
119
        $colorPalette = [];
120
        if (!empty($tempPath)) {
121
            // Extract the color palette
122
            try {
123
                $palette = ColorThief::getPalette($tempPath, 5);
124
            } catch (Exception $e) {
125
                Craft::error($e->getMessage(), __METHOD__);
126
127
                return [];
128
            }
129
            // Convert RGB to hex color
130
            foreach ($palette as $colors) {
131
                $colorPalette[] = sprintf('#%02x%02x%02x', $colors[0], $colors[1], $colors[2]);
132
            }
133
        }
134
        Craft::endProfile('generateColorPalette', __METHOD__);
135
136
        return $colorPalette;
137
    }
138
139
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
140
     * @param array $colors
0 ignored issues
show
Missing parameter comment
Loading history...
141
     *
142
     * @return float|int|null
143
     */
144
    public function calculateLightness(array $colors): float|int|null
145
    {
146
        $lightness = null;
147
        if (!empty($colors)) {
148
            $lightness = 0;
149
            $colorWeight = count($colors);
150
            $colorCount = 0;
151
            foreach ($colors as $color) {
152
                $rgb = ColorHelper::HTMLToRGB($color);
153
                $hsl = ColorHelper::RGBToHSL($rgb);
154
                $lightness += $hsl['l'] * $colorWeight;
155
                $colorCount += $colorWeight;
156
                $colorWeight--;
157
            }
158
159
            $lightness /= $colorCount;
160
        }
161
162
        return $lightness === null ? $lightness : (int)$lightness;
163
    }
164
165
    /**
166
     * Generate an SVG image via Potrace
167
     *
168
     * @param string $tempPath
0 ignored issues
show
Missing parameter comment
Loading history...
169
     *
170
     * @return string
171
     */
172
    public function generatePlaceholderSvg(string $tempPath): string
173
    {
174
        Craft::beginProfile('generatePlaceholderSvg', __METHOD__);
175
        $result = '';
176
177
        if (!empty($tempPath)) {
178
            // Potracio depends on `gd` being installed
179
            if (function_exists('imagecreatefromjpeg')) {
180
                $pot = new Potracio();
181
                $pot->loadImageFromFile($tempPath);
182
                $pot->process();
183
184
                $result = $pot->getSVG(1);
185
186
                // Optimize the result if we got one
187
                if (!empty($result)) {
188
                    $result = ImageOptimize::$plugin->optimizedImages->encodeOptimizedSVGDataUri($result);
189
                }
190
            }
191
            /** @var Settings $settings */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
192
            $settings = ImageOptimize::$plugin->getSettings();
0 ignored issues
show
The method getSettings() does not exist on null. ( Ignorable by Annotation )

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

192
            /** @scrutinizer ignore-call */ 
193
            $settings = ImageOptimize::$plugin->getSettings();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
193
            /**
194
             * If Potracio failed or gd isn't installed, or this is larger
195
             * than MAX_SILHOUETTE_SIZE bytes, just return a box
196
             */
197
            if (empty($result) || ((strlen($result) > self::MAX_SILHOUETTE_SIZE) && $settings->capSilhouetteSvgSize)) {
198
                $size = getimagesize($tempPath);
199
                if ($size !== false) {
200
                    [$width, $height] = $size;
201
                    $result = $this->generatePlaceholderBox($width, $height);
202
                }
203
            }
204
        }
205
        Craft::endProfile('generatePlaceholderSvg', __METHOD__);
206
207
        return $result;
208
    }
209
210
    /**
211
     * Create a small placeholder image file that the various placerholder
212
     * generators can use
213
     *
214
     * @param Asset $asset
0 ignored issues
show
Expected 13 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
215
     * @param float $aspectRatio
0 ignored issues
show
Expected 13 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
216
     * @param mixed|string|null $position
0 ignored issues
show
Missing parameter comment
Loading history...
217
     *
218
     * @return string
219
     */
220
    public function createTempPlaceholderImage(Asset $asset, float $aspectRatio, mixed $position): string
221
    {
222
        Craft::beginProfile('createTempPlaceholderImage', __METHOD__);
223
        Craft::info(
224
            'Creating temporary placeholder image for asset',
225
            __METHOD__
226
        );
227
        $width = self::TEMP_PLACEHOLDER_WIDTH;
228
        $height = (int)($width / $aspectRatio);
229
        $tempPath = $this->createImageFromAsset($asset, $width, $height, self::TEMP_PLACEHOLDER_QUALITY, $position);
230
        Craft::endProfile('createTempPlaceholderImage', __METHOD__);
231
232
        return $tempPath;
233
    }
234
235
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
236
     * @param Asset $asset
0 ignored issues
show
Expected 13 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
237
     * @param int $width
0 ignored issues
show
Expected 15 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
238
     * @param int $height
0 ignored issues
show
Expected 15 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
239
     * @param int $quality
0 ignored issues
show
Expected 15 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
240
     * @param mixed|string|null $position
0 ignored issues
show
Missing parameter comment
Loading history...
241
     *
242
     * @return string
243
     */
244
    public function createImageFromAsset(Asset $asset, int $width, int $height, int $quality, mixed $position): string
245
    {
246
        $tempPath = '';
247
        if (Image::canManipulateAsImage($asset->getExtension())) {
248
            $imageSource = TransformHelper::getLocalImageSource($asset);
249
            // Scale and crop the placeholder image
250
            $tempPath = $this->createImageFromPath($imageSource, $width, $height, $quality, $position);
251
        }
252
253
        return $tempPath;
254
    }
255
256
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
257
     * @param string $filePath
0 ignored issues
show
Expected 12 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
258
     * @param int $width
0 ignored issues
show
Expected 15 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
259
     * @param int $height
0 ignored issues
show
Expected 15 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
260
     * @param int $quality
0 ignored issues
show
Expected 15 spaces after parameter type; 1 found
Loading history...
Missing parameter comment
Loading history...
261
     * @param mixed|string|null $position
0 ignored issues
show
Missing parameter comment
Loading history...
262
     *
263
     * @return string
264
     */
265
    public function createImageFromPath(
266
        string $filePath,
267
        int    $width,
268
        int    $height,
269
        int    $quality,
270
        mixed  $position,
271
    ): string {
272
        $images = Craft::$app->getImages();
273
        $pathParts = pathinfo($filePath);
274
        try {
275
            if (StringHelper::toLowerCase($pathParts['extension']) === 'svg') {
276
                $image = $images->loadImage($filePath, true, $width);
277
            } else {
278
                $image = $images->loadImage($filePath);
279
            }
280
        } catch (Throwable $e) {
281
            Craft::error(
282
                'Error creating temporary image: ' . $e->getMessage(),
283
                __METHOD__
284
            );
285
286
            return '';
287
        }
288
289
        if ($image instanceof Raster) {
290
            $image->setQuality($quality);
291
        }
292
293
        // Resize the image
294
        $image->scaleAndCrop($width, $height, true, $position);
295
296
        // Strip any EXIF data from the image before trying to save it
297
        if ($image instanceof Raster) {
298
            $imagineImage = $image->getImagineImage();
299
            if ($imagineImage) {
0 ignored issues
show
$imagineImage is of type Imagine\Image\AbstractImage, thus it always evaluated to true.
Loading history...
300
                $imagineImage->strip();
301
            }
302
        }
303
304
305
        // Save the image out to a temp file, then return its contents
306
        $tempFilename = uniqid(pathinfo($pathParts['filename'], PATHINFO_FILENAME), true) . '.' . 'jpg';
0 ignored issues
show
It seems like pathinfo($pathParts['fil...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

306
        $tempFilename = uniqid(/** @scrutinizer ignore-type */ pathinfo($pathParts['filename'], PATHINFO_FILENAME), true) . '.' . 'jpg';
Loading history...
307
        $tempPath = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . $tempFilename;
308
        clearstatcache(true, $tempPath);
309
        try {
310
            $image->saveAs($tempPath);
311
        } catch (Throwable $e) {
312
            Craft::error(
313
                'Error saving temporary image: ' . $e->getMessage(),
314
                __METHOD__
315
            );
316
        }
317
318
        return $tempPath;
319
    }
320
}
321