Passed
Push — develop ( a40575...26f189 )
by Andrew
03:50
created

OptimizedImage::src()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
/**
3
 * Image Optimize 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\models;
12
13
use nystudio107\imageoptimize\ImageOptimize;
14
use nystudio107\imageoptimize\helpers\UrlHelper;
15
16
use craft\helpers\Template;
17
use craft\base\Model;
18
use craft\validators\ArrayValidator;
19
20
/**
21
 * @author    nystudio107
22
 * @package   ImageOptimize
23
 * @since     1.2.0
24
 */
25
class OptimizedImage extends Model
26
{
27
    // Public Properties
28
    // =========================================================================
29
30
    /**
31
     * @var array
32
     */
33
    public $optimizedImageUrls = [];
34
35
    /**
36
     * @var array
37
     */
38
    public $optimizedWebPImageUrls = [];
39
40
    /**
41
     * @var array
42
     */
43
    public $variantSourceWidths = [];
44
45
    /**
46
     * @var array
47
     */
48
    public $focalPoint;
49
50
    /**
51
     * @var int
52
     */
53
    public $originalImageWidth;
54
55
    /**
56
     * @var int
57
     */
58
    public $originalImageHeight;
59
60
    /**
61
     * @var string
62
     */
63
    public $placeholder = '';
64
65
    /**
66
     * @var
67
     */
68
    public $placeholderSvg = '';
69
70
    /**
71
     * @var array
72
     */
73
    public $colorPalette = [];
74
75
    /**
76
     * @var int
77
     */
78
    public $placeholderWidth;
79
80
    /**
81
     * @var int
82
     */
83
    public $placeholderHeight;
84
85
    // Public Methods
86
    // =========================================================================
87
88
    /**
89
     * @inheritdoc
90
     */
91
    public function rules()
92
    {
93
        return [
94
            ['optimizedImageUrls', ArrayValidator::class],
95
            ['optimizedWebPImageUrls', ArrayValidator::class],
96
            ['variantSourceWidths', ArrayValidator::class],
97
            ['focalPoint', 'safe'],
98
            ['originalImageWidth', 'integer'],
99
            ['originalImageHeight', 'integer'],
100
            ['placeholder', 'string'],
101
            ['placeholderSvg', 'string'],
102
            ['colorPalette', ArrayValidator::class],
103
            ['placeholderWidth', 'integer'],
104
            ['placeholderHeight', 'integer'],
105
        ];
106
    }
107
108
    /**
109
     * Return the first image variant URL or the specific one passed in via
110
     * $width
111
     *
112
     * @param int $width
113
     *
114
     * @return \Twig_Markup|null
115
     */
116
    public function src(int $width = 0): string
117
    {
118
        if (empty($width)) {
119
            return Template::raw(reset($this->optimizedImageUrls));
120
        } else {
121
            return Template::raw($this->optimizedImageUrls[$width] ?? '');
122
        }
123
    }
124
125
    /**
126
     * Return a string of image URLs and their sizes
127
     *
128
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
129
     *                  srcsets
130
     *
131
     * @return \Twig_Markup|null
132
     */
133
    public function srcset(bool $dpr = false): string
134
    {
135
        return Template::raw($this->getSrcsetFromArray($this->optimizedImageUrls, $dpr));
136
    }
137
138
    /**
139
     * Return a string of image URLs and their sizes that match $width
140
     *
141
     * @param int  $width
142
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
143
     *                  srcsets
144
     *
145
     * @return \Twig_Markup|null
146
     */
147
    public function srcsetWidth(int $width, bool $dpr = false): string
148
    {
149
        $subset = $this->getSrcsetSubsetArray($this->optimizedImageUrls, $width, 'width');
150
151
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
152
    }
153
154
    /**
155
     * Return a string of image URLs and their sizes that are at least $width
156
     * or larger
157
     *
158
     * @param int  $width
159
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
160
     *                  srcsets
161
     *
162
     * @return \Twig_Markup|null
163
     */
164
    public function srcsetMinWidth(int $width, bool $dpr = false): string
165
    {
166
        $subset = $this->getSrcsetSubsetArray($this->optimizedImageUrls, $width, 'minwidth');
167
168
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
169
    }
170
171
    /**
172
     * Return a string of image URLs and their sizes that are $width or smaller
173
     *
174
     * @param int  $width
175
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
176
     *                  srcsets
177
     *
178
     * @return \Twig_Markup|null
179
     */
180
    public function srcsetMaxWidth(int $width, bool $dpr = false): string
181
    {
182
        $subset = $this->getSrcsetSubsetArray($this->optimizedImageUrls, $width, 'maxwidth');
183
184
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
185
    }
186
187
    /**
188
     * Return the first webp image variant URL or the specific one passed in
189
     * via $width
190
     *
191
     * @param int $width
192
     *
193
     * @return \Twig_Markup|null
194
     */
195
    public function srcWebp(int $width = 0): string
196
    {
197
        if (empty($width)) {
198
            return Template::raw(reset($this->optimizedWebPImageUrls));
199
        } else {
200
            return Template::raw($this->optimizedWebPImageUrls[$width] ?? '');
201
        }
202
    }
203
204
    /**
205
     * Return a string of webp image URLs and their sizes
206
     *
207
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
208
     *                  srcsets
209
     *
210
     * @return \Twig_Markup|null
211
     */
212
    public function srcsetWebp(bool $dpr = false): string
213
    {
214
        return Template::raw($this->getSrcsetFromArray($this->optimizedWebPImageUrls, $dpr));
215
    }
216
217
    /**
218
     * Return a string of webp image URLs and their sizes that match $width
219
     *
220
     * @param int  $width
221
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
222
     *                  srcsets
223
     *
224
     * @return \Twig_Markup|null
225
     */
226
    public function srcsetWidthWebp(int $width, bool $dpr = false): string
227
    {
228
        $subset = $this->getSrcsetSubsetArray($this->optimizedWebPImageUrls, $width, 'width');
229
230
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
231
    }
232
233
    /**
234
     * Return a string of webp image URLs and their sizes that are at least
235
     * $width or larger
236
     *
237
     * @param int  $width
238
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
239
     *                  srcsets
240
     *
241
     * @return \Twig_Markup|null
242
     */
243
    public function srcsetMinWidthWebp(int $width, bool $dpr = false): string
244
    {
245
        $subset = $this->getSrcsetSubsetArray($this->optimizedWebPImageUrls, $width, 'minwidth');
246
247
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
248
    }
249
250
    /**
251
     * Return a string of webp image URLs and their sizes that are $width or
252
     * smaller
253
     *
254
     * @param int  $width
255
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
256
     *                  srcsets
257
     *
258
     * @return \Twig_Markup|null
259
     */
260
    public function srcsetMaxWidthWebp(int $width, bool $dpr = false): string
261
    {
262
        $subset = $this->getSrcsetSubsetArray($this->optimizedWebPImageUrls, $width, 'maxwidth');
263
264
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
265
    }
266
267
    /**
268
     * Work around issues with `<img srcset>` returning sizes larger than are
269
     * available as per:
270
     * https://medium.com/@MRWwebDesign/responsive-images-the-sizes-attribute-and-unexpected-image-sizes-882a2eadb6db
271
     *
272
     * @return int
273
     */
274
    public function maxSrcsetWidth(): int
275
    {
276
        $result = 0;
277
        if (!empty($this->optimizedImageUrls)) {
278
            $tempArray = $this->optimizedImageUrls;
279
            ksort($tempArray, SORT_NUMERIC);
280
281
            $keys = array_keys($tempArray);
282
            $result = end($keys);
283
        }
284
285
        return $result;
286
    }
287
288
    /**
289
     * Return a base64-encoded placeholder image
290
     *
291
     * @return \Twig_Markup|null
292
     */
293
    public function placeholderImage()
294
    {
295
        $header = 'data:image/jpeg;base64,';
296
        if (!empty($this->placeholder)) {
297
            $content = $this->placeholder;
298
        } else {
299
            // At least return something
300
            return $this->defaultPlaceholderImage();
301
        }
302
303
        return Template::raw($header.rawurlencode($content));
304
    }
305
306
    /**
307
     * @return string
308
     */
309
    public function placeholderImageSize()
310
    {
311
        $placeholder = $this->placeholderImage();
312
        $contentLength = !empty(strlen($placeholder)) ? strlen($placeholder) : 0;
313
314
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
315
    }
316
317
    /**
318
     * Return an SVG box as a placeholder image
319
     *
320
     * @param string|null $color
321
     *
322
     * @return \Twig_Markup|null
323
     */
324
    public function placeholderBox($color = null)
325
    {
326
        $width = $this->placeholderWidth ?? 1;
327
        $height = $this->placeholderHeight ?? 1;
328
        $color = $color ?? $this->colorPalette[0] ?? '#CCC';
329
330
        return Template::raw(ImageOptimize::$plugin->placeholder->generatePlaceholderBox($width, $height, $color));
331
    }
332
333
    /**
334
     * @return string
335
     */
336
    public function placeholderBoxSize()
337
    {
338
        $placeholder = $this->placeholderBox();
339
        $contentLength = !empty(strlen($placeholder)) ? strlen($placeholder) : 0;
340
341
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
342
    }
343
344
    /**
345
     * Return a silhouette of the image as an SVG placeholder
346
     *
347
     * @return \Twig_Markup|null
348
     */
349
    public function placeholderSilhouette()
350
    {
351
        $header = 'data:image/svg+xml,';
352
        if (!empty($this->placeholderSvg)) {
353
            $content = $this->placeholderSvg;
354
        } else {
355
            // At least return something
356
            return $this->defaultPlaceholderImage();
357
        }
358
359
        return Template::raw($header.$content);
360
    }
361
362
    /**
363
     * @return string
364
     */
365
    public function placeholderSilhouetteSize()
366
    {
367
        $placeholder = $this->placeholderSilhouette();
368
        $contentLength = !empty(strlen($placeholder)) ? strlen($placeholder) : 0;
369
370
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
371
    }
372
373
    /**
374
     *  Get the file size of any remote resource (using curl),
375
     *  either in bytes or - default - as human-readable formatted string.
376
     *
377
     * @author  Stephan Schmitz <[email protected]>
378
     * @license MIT <http://eyecatchup.mit-license.org/>
379
     * @url     <https://gist.github.com/eyecatchup/f26300ffd7e50a92bc4d>
380
     *
381
     * @param   string  $url        Takes the remote object's URL.
382
     * @param   boolean $formatSize Whether to return size in bytes or
383
     *                              formatted.
384
     * @param   boolean $useHead    Whether to use HEAD requests. If false,
385
     *                              uses GET.
386
     *
387
     * @return  int|mixed|string    Returns human-readable formatted size
388
     *                              or size in bytes (default: formatted).
389
     */
390
    public function getRemoteFileSize($url, $formatSize = true, $useHead = true)
391
    {
392
        // Get an absolute URL with protocol that curl will be happy with
393
        $url = UrlHelper::absoluteUrlWithProtocol($url);
394
        $ch = curl_init($url);
395
        curl_setopt_array($ch, [
396
            CURLOPT_RETURNTRANSFER => 1,
397
            CURLOPT_FOLLOWLOCATION => 1,
398
            CURLOPT_SSL_VERIFYPEER => 0,
399
        ]);
400
        if ($useHead) {
401
            curl_setopt($ch, CURLOPT_NOBODY, 1);
402
        }
403
        curl_exec($ch);
404
        // content-length of download (in bytes), read from Content-Length: field
405
        $contentLength = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
406
        curl_close($ch);
407
        // cannot retrieve file size, return "-1"
408
        if (!$contentLength) {
409
            return -1;
410
        }
411
        // return size in bytes
412
        if (!$formatSize) {
413
            return $contentLength;
414
        }
415
416
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
417
    }
418
419
    // Protected Methods
420
    // =========================================================================
421
422
    protected function getSrcsetSubsetArray(array $set, int $width, string $comparison): array
423
    {
424
        $subset = [];
425
        $index = 0;
426
        if (empty($this->variantSourceWidths)) {
427
            return $subset;
428
        }
429
        foreach ($this->variantSourceWidths as $variantSourceWidth) {
430
            $match = false;
431
            switch ($comparison) {
432
                case 'width':
433
                    if ($variantSourceWidth == $width) {
434
                        $match = true;
435
                    }
436
                    break;
437
438
                case 'minwidth':
439
                    if ($variantSourceWidth >= $width) {
440
                        $match = true;
441
                    }
442
                    break;
443
444
                case 'maxwidth':
445
                    if ($variantSourceWidth <= $width) {
446
                        $match = true;
447
                    }
448
                    break;
449
            }
450
            if ($match) {
451
                $subset += array_slice($set, $index, 1, true);
452
            }
453
            $index++;
454
        }
455
456
        return $subset;
457
    }
458
459
    /**
460
     * @param array $array
461
     * @param bool  $dpr
462
     *
463
     * @return string
464
     */
465
    protected function getSrcsetFromArray(array $array, bool $dpr = false): string
466
    {
467
        $srcset = '';
468
        foreach ($array as $key => $value) {
469
            if ($dpr) {
470
                $descriptor = '1x';
471
                if (!empty($array[intval($key) / 2])) {
472
                    $descriptor = '2x';
473
                }
474
                if (!empty($array[intval($key) / 3])) {
475
                    $descriptor = '3x';
476
                }
477
            } else {
478
                $descriptor = $key.'w';
479
            }
480
            $srcset .= $value.' '.$descriptor.', ';
481
        }
482
        $srcset = rtrim($srcset, ', ');
483
484
        return $srcset;
485
    }
486
487
    /**
488
     * Return a default placeholder image
489
     *
490
     * @return \Twig_Markup
491
     */
492
    protected function defaultPlaceholderImage(): \Twig_Markup
493
    {
494
        $width = 1;
495
        $height = 1;
496
        $color = '#CCC';
497
498
        return Template::raw(ImageOptimize::$plugin->placeholder->generatePlaceholderBox($width, $height, $color));
499
    }
500
}
501