Passed
Push — develop ( 95f65a...4503d2 )
by Andrew
04:41
created

Placeholder::generatePlaceholderBox()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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\Color as ColorHelper;
15
use nystudio107\imageoptimize\lib\Potracio;
16
17
use Craft;
18
use craft\base\Component;
19
use craft\elements\Asset;
20
use craft\errors\ImageException;
21
use craft\helpers\Image;
22
use craft\helpers\StringHelper;
23
use craft\image\Raster;
24
25
use ColorThief\ColorThief;
26
27
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
28
 * @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...
29
 * @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...
30
 * @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...
31
 */
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...
32
class Placeholder extends Component
33
{
34
    // Constants
35
    // =========================================================================
36
37
    const PLACEHOLDER_WIDTH = 16;
38
    const PLACEHOLDER_QUALITY = 50;
39
40
    const TEMP_PLACEHOLDER_WIDTH = 300;
41
    const TEMP_PLACEHOLDER_QUALITY = 75;
42
43
    const MAX_SILHOUETTE_SIZE = 30 * 1024;
44
45
    // Public Properties
46
    // =========================================================================
47
48
    // Public Methods
49
    // =========================================================================
50
51
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $width should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $height should have a doc-comment as per coding-style.
Loading history...
52
     * Return an SVG box as a placeholder image
53
     *
54
     * @param             $width
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 13
Loading history...
55
     * @param             $height
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 13
Loading history...
56
     * @param string|null $color
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
57
     *
58
     * @return string
59
     */
60
    public function generatePlaceholderBox($width, $height, $color = null): string
61
    {
62
        $color = $color ?? '#CCC';
63
        $header = 'data:image/svg+xml,';
64
        $content = "<svg xmlns='http://www.w3.org/2000/svg' "
65
            ."width='$width' "
66
            ."height='$height' "
67
            ."style='background:$color' "
68
            ."/>";
69
70
        return $header.ImageOptimize::$plugin->optimizedImages->encodeOptimizedSVGDataUri($content);
71
    }
72
73
    /**
74
     * Generate a base64-encoded placeholder image
75
     *
76
     * @param string            $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
77
     * @param float             $aspectRatio
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
78
     * @param mixed|string|null $position
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
79
     *
80
     * @return string
81
     */
82
    public function generatePlaceholderImage(string $tempPath, float $aspectRatio, $position): string
83
    {
84
        Craft::beginProfile('generatePlaceholderImage', __METHOD__);
85
        $result = '';
86
        $width = self::PLACEHOLDER_WIDTH;
87
        $height = (int)($width / $aspectRatio);
88
        $placeholderPath = $this->createImageFromPath($tempPath, $width, $height, self::PLACEHOLDER_QUALITY, $position);
89
        if (!empty($placeholderPath)) {
90
            $result = base64_encode(file_get_contents($placeholderPath));
91
            unlink($placeholderPath);
92
        }
93
        Craft::endProfile('generatePlaceholderImage', __METHOD__);
94
95
        return $result;
96
    }
97
98
    /**
99
     * Generate a color palette from the image
100
     *
101
     * @param string $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
102
     *
103
     * @return array
104
     */
105
    public function generateColorPalette(string $tempPath): array
106
    {
107
        Craft::beginProfile('generateColorPalette', __METHOD__);
108
        $colorPalette = [];
109
        if (!empty($tempPath)) {
110
            // Extract the color palette
111
            try {
112
                $palette = ColorThief::getPalette($tempPath, 5);
113
            } catch (\Exception $e) {
114
                Craft::error($e->getMessage(), __METHOD__);
115
116
                return [];
117
            }
118
            // Convert RGB to hex color
119
            foreach ($palette as $colors) {
120
                $colorPalette[] = sprintf('#%02x%02x%02x', $colors[0], $colors[1], $colors[2]);
121
            }
122
        }
123
        Craft::endProfile('generateColorPalette', __METHOD__);
124
125
        return $colorPalette;
126
    }
127
128
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
129
     * @param array $colors
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
130
     *
131
     * @return float|int|null
132
     */
133
    public function calculateLightness(array $colors)
134
    {
135
        $lightness = null;
136
        if (!empty($colors)) {
137
            $lightness = 0;
138
            $colorWeight = count($colors);
139
            $colorCount = 0;
140
            foreach ($colors as $color) {
141
                $rgb = ColorHelper::HTMLToRGB($color);
142
                $hsl = ColorHelper::RGBToHSL($rgb);
143
                $lightness += $hsl['l'] * $colorWeight;
144
                $colorCount += $colorWeight;
145
                $colorWeight--;
146
            }
147
148
            $lightness /= $colorCount;
149
        }
150
151
        return $lightness === null ? $lightness : (int)$lightness;
152
    }
153
    /**
154
     * Generate an SVG image via Potrace
155
     *
156
     * @param string $tempPath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
157
     *
158
     * @return string
159
     */
160
    public function generatePlaceholderSvg(string $tempPath): string
161
    {
162
        Craft::beginProfile('generatePlaceholderSvg', __METHOD__);
163
        $result = '';
164
165
        if (!empty($tempPath)) {
166
            // Potracio depends on `gd` being installed
167
            if (\function_exists('imagecreatefromjpeg')) {
168
                $pot = new Potracio();
169
                $pot->loadImageFromFile($tempPath);
170
                $pot->process();
171
172
                $result = $pot->getSVG(1);
173
174
                // Optimize the result if we got one
175
                if (!empty($result)) {
176
                    $result = ImageOptimize::$plugin->optimizedImages->encodeOptimizedSVGDataUri($result);
177
                }
178
            }
179
            /**
180
             * If Potracio failed or gd isn't installed, or this is larger
181
             * than MAX_SILHOUETTE_SIZE bytes, just return a box
182
             */
183
            if (empty($result) || (\strlen($result) > self::MAX_SILHOUETTE_SIZE)) {
184
                $size = getimagesize($tempPath);
185
                if ($size !== false) {
186
                    list($width, $height) = $size;
187
                    $result = $this->generatePlaceholderBox($width, $height);
188
                }
189
            }
190
        }
191
        Craft::endProfile('generatePlaceholderSvg', __METHOD__);
192
193
        return $result;
194
    }
195
196
    /**
197
     * Create a small placeholder image file that the various placerholder
198
     * generators can use
199
     *
200
     * @param Asset             $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
201
     * @param float             $aspectRatio
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
202
     * @param mixed|string|null $position
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
203
     *
204
     * @return string
205
     */
206
    public function createTempPlaceholderImage(Asset $asset, float $aspectRatio, $position): string
207
    {
208
        Craft::beginProfile('createTempPlaceholderImage', __METHOD__);
209
        $width = self::TEMP_PLACEHOLDER_WIDTH;
210
        $height = (int)($width / $aspectRatio);
211
        $tempPath = $this->createImageFromAsset($asset, $width, $height, self::TEMP_PLACEHOLDER_QUALITY, $position);
212
        Craft::endProfile('createTempPlaceholderImage', __METHOD__);
213
214
        return $tempPath;
215
    }
216
217
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
218
     * @param Asset             $asset
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
219
     * @param int               $width
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
220
     * @param int               $height
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
221
     * @param int               $quality
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
222
     * @param mixed|string|null $position
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
223
     *
224
     * @return string
225
     */
226
    public function createImageFromAsset(Asset $asset, int $width, int $height, int $quality, $position): string
227
    {
228
        $tempPath = '';
229
230
        if ($asset !== null && Image::canManipulateAsImage($asset->getExtension())) {
231
            $imageSource = $asset->getTransformSource();
232
            // Scale and crop the placeholder image
233
            $tempPath = $this->createImageFromPath($imageSource, $width, $height, $quality, $position);
234
        }
235
236
        return $tempPath;
237
    }
238
239
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
240
     * @param string            $filePath
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
241
     * @param int               $width
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
242
     * @param int               $height
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
243
     * @param int               $quality
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
244
     * @param mixed|string|null $position
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
245
     *
246
     * @return string
247
     */
248
    public function createImageFromPath(
249
        string $filePath,
250
        int $width,
251
        int $height,
252
        int $quality,
253
        $position
254
    ): string {
255
        $images = Craft::$app->getImages();
256
        $pathParts = pathinfo($filePath);
257
        /** @var Image $image */
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...
258
        if (StringHelper::toLowerCase($pathParts['extension']) === 'svg') {
259
            $image = $images->loadImage($filePath, true, $width);
260
        } else {
261
            $image = $images->loadImage($filePath);
262
        }
263
264
        if ($image instanceof Raster) {
265
            $image->setQuality($quality);
266
        }
267
268
        // No matter what the user settings are, we don't want any metadata/color palette cruft
269
        $config = Craft::$app->getConfig()->getGeneral();
270
        $oldOptimizeImageFilesize = $config->optimizeImageFilesize;
271
        $oldPreserveImageColorProfiles = $config->preserveImageColorProfiles;
272
        $oldPreserveExifData = $config->preserveExifData;
273
        $config->optimizeImageFilesize = true;
274
        $config->preserveImageColorProfiles = false;
275
        $config->preserveExifData = false;
276
277
        // Resize the image
278
        $image->scaleAndCrop($width, $height, true, $position);
279
280
        // Restore the old settings
281
        $config->optimizeImageFilesize = $oldOptimizeImageFilesize;
282
        $config->preserveImageColorProfiles = $oldPreserveImageColorProfiles;
283
        $config->preserveExifData = $oldPreserveExifData;
284
285
        // Save the image out to a temp file, then return its contents
286
        $tempFilename = uniqid(pathinfo($pathParts['filename'], PATHINFO_FILENAME), true).'.'.'jpg';
287
        $tempPath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$tempFilename;
288
        clearstatcache(true, $tempPath);
289
        try {
290
            $image->saveAs($tempPath);
291
        } catch (ImageException $e) {
292
            Craft::error(
293
                'Error saving temporary image: '.$e->getMessage(),
294
                __METHOD__
295
            );
296
        }
297
298
        return $tempPath;
299
    }
300
}
301