Passed
Push — develop ( 4ac642...83abb8 )
by Andrew
03:28
created

OptimizedImage::getSrcWebp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
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(): array
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
        }
121
122
        return Template::raw($this->optimizedImageUrls[$width] ?? '');
123
    }
124
125
    /**
126
     * Getter for CraftQL
127
     *
128
     * @param int $width
129
     *
130
     * @return null|string|\Twig_Markup
131
     */
132
    public function getSrc(int $width = 0): string
133
    {
134
        return $this->src($width);
135
    }
136
137
    /**
138
     * Return a string of image URLs and their sizes
139
     *
140
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
141
     *                  srcsets
142
     *
143
     * @return \Twig_Markup|null
144
     */
145
    public function srcset(bool $dpr = false): string
146
    {
147
        return Template::raw($this->getSrcsetFromArray($this->optimizedImageUrls, $dpr));
148
    }
149
150
    /**
151
     * Getter for CraftQL
152
     *
153
     * @param bool $dpr
154
     *
155
     * @return string
156
     */
157
    public function getSrcset(bool $dpr = false): string
158
    {
159
        return $this->srcset($dpr);
160
    }
161
    /**
162
     * Return a string of image URLs and their sizes that match $width
163
     *
164
     * @param int  $width
165
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
166
     *                  srcsets
167
     *
168
     * @return \Twig_Markup|null
169
     */
170
    public function srcsetWidth(int $width, bool $dpr = false): string
171
    {
172
        $subset = $this->getSrcsetSubsetArray($this->optimizedImageUrls, $width, 'width');
173
174
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
175
    }
176
177
    /**
178
     * Return a string of image URLs and their sizes that are at least $width
179
     * or larger
180
     *
181
     * @param int  $width
182
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
183
     *                  srcsets
184
     *
185
     * @return \Twig_Markup|null
186
     */
187
    public function srcsetMinWidth(int $width, bool $dpr = false): string
188
    {
189
        $subset = $this->getSrcsetSubsetArray($this->optimizedImageUrls, $width, 'minwidth');
190
191
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
192
    }
193
194
    /**
195
     * Return a string of image URLs and their sizes that are $width or smaller
196
     *
197
     * @param int  $width
198
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
199
     *                  srcsets
200
     *
201
     * @return \Twig_Markup|null
202
     */
203
    public function srcsetMaxWidth(int $width, bool $dpr = false): string
204
    {
205
        $subset = $this->getSrcsetSubsetArray($this->optimizedImageUrls, $width, 'maxwidth');
206
207
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
208
    }
209
210
    /**
211
     * Return the first webp image variant URL or the specific one passed in
212
     * via $width
213
     *
214
     * @param int $width
215
     *
216
     * @return \Twig_Markup|null
217
     */
218
    public function srcWebp(int $width = 0): string
219
    {
220
        if (empty($width)) {
221
            return Template::raw(reset($this->optimizedWebPImageUrls));
222
        }
223
224
        return Template::raw($this->optimizedWebPImageUrls[$width] ?? '');
225
    }
226
227
    /**
228
     * Getter for CraftQL
229
     *
230
     * @param int $width
231
     *
232
     * @return string
233
     */
234
    public function getSrcWebp(int $width = 0): string
235
    {
236
        return $this->srcWebp($width);
237
    }
238
239
    /**
240
     * Return a string of webp image URLs and their sizes
241
     *
242
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
243
     *                  srcsets
244
     *
245
     * @return \Twig_Markup|null
246
     */
247
    public function srcsetWebp(bool $dpr = false): string
248
    {
249
        return Template::raw($this->getSrcsetFromArray($this->optimizedWebPImageUrls, $dpr));
250
    }
251
252
    /**
253
     * Getter for CraftQL
254
     *
255
     * @param bool $dpr
256
     *
257
     * @return string
258
     */
259
    public function getSrcsetWebp(bool $dpr = false): string
260
    {
261
        return $this->srcsetWebp($dpr);
262
    }
263
264
    /**
265
     * Return a string of webp image URLs and their sizes that match $width
266
     *
267
     * @param int  $width
268
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
269
     *                  srcsets
270
     *
271
     * @return \Twig_Markup|null
272
     */
273
    public function srcsetWidthWebp(int $width, bool $dpr = false): string
274
    {
275
        $subset = $this->getSrcsetSubsetArray($this->optimizedWebPImageUrls, $width, 'width');
276
277
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
278
    }
279
280
    /**
281
     * Return a string of webp image URLs and their sizes that are at least
282
     * $width or larger
283
     *
284
     * @param int  $width
285
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
286
     *                  srcsets
287
     *
288
     * @return \Twig_Markup|null
289
     */
290
    public function srcsetMinWidthWebp(int $width, bool $dpr = false): string
291
    {
292
        $subset = $this->getSrcsetSubsetArray($this->optimizedWebPImageUrls, $width, 'minwidth');
293
294
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
295
    }
296
297
    /**
298
     * Return a string of webp image URLs and their sizes that are $width or
299
     * smaller
300
     *
301
     * @param int  $width
302
     * @param bool $dpr Whether to generate 1x, 2x srcsets vs the normal XXXw
303
     *                  srcsets
304
     *
305
     * @return \Twig_Markup|null
306
     */
307
    public function srcsetMaxWidthWebp(int $width, bool $dpr = false): string
308
    {
309
        $subset = $this->getSrcsetSubsetArray($this->optimizedWebPImageUrls, $width, 'maxwidth');
310
311
        return Template::raw($this->getSrcsetFromArray($subset, $dpr));
312
    }
313
314
    /**
315
     * Work around issues with `<img srcset>` returning sizes larger than are
316
     * available as per:
317
     * https://medium.com/@MRWwebDesign/responsive-images-the-sizes-attribute-and-unexpected-image-sizes-882a2eadb6db
318
     *
319
     * @return int
320
     */
321
    public function maxSrcsetWidth(): int
322
    {
323
        $result = 0;
324
        if (!empty($this->optimizedImageUrls)) {
325
            $tempArray = $this->optimizedImageUrls;
326
            ksort($tempArray, SORT_NUMERIC);
327
328
            $keys = array_keys($tempArray);
329
            $result = end($keys);
330
        }
331
332
        return $result;
333
    }
334
335
    /**
336
     * Getter for CraftQL
337
     *
338
     * @return int
339
     */
340
    public function getMaxSrcsetWidth(): int
341
    {
342
        return $this->maxSrcsetWidth();
343
    }
344
345
    /**
346
     * Return a base64-encoded placeholder image
347
     *
348
     * @return \Twig_Markup|null
349
     */
350
    public function placeholderImage()
351
    {
352
        $header = 'data:image/jpeg;base64,';
353
        if (!empty($this->placeholder)) {
354
            $content = $this->placeholder;
355
        } else {
356
            // At least return something
357
            return $this->defaultPlaceholderImage();
358
        }
359
360
        return Template::raw($header.rawurlencode($content));
361
    }
362
363
    /**
364
     * Getter for CraftQL
365
     *
366
     * @return string
367
     */
368
    public function getPlaceholderImage(): string
369
    {
370
        return (string)$this->placeholderImage();
371
    }
372
373
    /**
374
     * @return string
375
     */
376
    public function placeholderImageSize(): string
377
    {
378
        $placeholder = $this->placeholderImage();
379
        $contentLength = !empty(\strlen($placeholder)) ? \strlen($placeholder) : 0;
380
381
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
382
    }
383
384
    /**
385
     * Return an SVG box as a placeholder image
386
     *
387
     * @param string|null $color
388
     *
389
     * @return \Twig_Markup|null
390
     */
391
    public function placeholderBox(string $color = null)
392
    {
393
        $width = $this->placeholderWidth ?? 1;
394
        $height = $this->placeholderHeight ?? 1;
395
        $color = $color ?? $this->colorPalette[0] ?? '#CCC';
396
397
        return Template::raw(ImageOptimize::$plugin->placeholder->generatePlaceholderBox($width, $height, $color));
398
    }
399
400
    /**
401
     * @param string|null $color
402
     *
403
     * @return string
404
     */
405
    public function getPlaceholderBox(string $color = null): string
406
    {
407
        return (string)$this->placeholderBox($color);
408
    }
409
410
    /**
411
     * Getter for CraftQL
412
     *
413
     * @return string
414
     */
415
    public function placeholderBoxSize(): string
416
    {
417
        $placeholder = $this->placeholderBox();
418
        $contentLength = !empty(\strlen($placeholder)) ? \strlen($placeholder) : 0;
419
420
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
421
    }
422
423
    /**
424
     * Return a silhouette of the image as an SVG placeholder
425
     *
426
     * @return \Twig_Markup|null
427
     */
428
    public function placeholderSilhouette()
429
    {
430
        $header = 'data:image/svg+xml,';
431
        if (!empty($this->placeholderSvg)) {
432
            $content = $this->placeholderSvg;
433
        } else {
434
            // At least return something
435
            return $this->defaultPlaceholderImage();
436
        }
437
438
        return Template::raw($header.$content);
439
    }
440
441
    /**
442
     * Getter for CraftQL
443
     *
444
     * @return string
445
     */
446
    public function getPlaceholderSilhouette(): string
447
    {
448
        return (string)$this->placeholderSilhouette();
449
    }
450
451
    /**
452
     * @return string
453
     */
454
    public function placeholderSilhouetteSize(): string
455
    {
456
        $placeholder = $this->placeholderSilhouette();
457
        $contentLength = !empty(\strlen($placeholder)) ? \strlen($placeholder) : 0;
458
459
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
460
    }
461
462
    /**
463
     *  Get the file size of any remote resource (using curl),
464
     *  either in bytes or - default - as human-readable formatted string.
465
     *
466
     * @author  Stephan Schmitz <[email protected]>
467
     * @license MIT <http://eyecatchup.mit-license.org/>
468
     * @url     <https://gist.github.com/eyecatchup/f26300ffd7e50a92bc4d>
469
     *
470
     * @param   string  $url        Takes the remote object's URL.
471
     * @param   boolean $formatSize Whether to return size in bytes or
472
     *                              formatted.
473
     * @param   boolean $useHead    Whether to use HEAD requests. If false,
474
     *                              uses GET.
475
     *
476
     * @return  int|mixed|string    Returns human-readable formatted size
477
     *                              or size in bytes (default: formatted).
478
     */
479
    public function getRemoteFileSize($url, $formatSize = true, $useHead = true)
480
    {
481
        // Get an absolute URL with protocol that curl will be happy with
482
        $url = UrlHelper::absoluteUrlWithProtocol($url);
483
        $ch = curl_init($url);
484
        curl_setopt_array($ch, [
485
            CURLOPT_RETURNTRANSFER => 1,
486
            CURLOPT_FOLLOWLOCATION => 1,
487
            CURLOPT_SSL_VERIFYPEER => 0,
488
        ]);
489
        if ($useHead) {
490
            curl_setopt($ch, CURLOPT_NOBODY, 1);
491
        }
492
        curl_exec($ch);
493
        // content-length of download (in bytes), read from Content-Length: field
494
        $contentLength = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
495
        curl_close($ch);
496
        // cannot retrieve file size, return "-1"
497
        if (!$contentLength) {
498
            return -1;
499
        }
500
        // return size in bytes
501
        if (!$formatSize) {
502
            return $contentLength;
503
        }
504
505
        return ImageOptimize::$plugin->optimize->humanFileSize($contentLength, 1);
506
    }
507
508
    // Protected Methods
509
    // =========================================================================
510
511
    protected function getSrcsetSubsetArray(array $set, int $width, string $comparison): array
512
    {
513
        $subset = [];
514
        $index = 0;
515
        if (empty($this->variantSourceWidths)) {
516
            return $subset;
517
        }
518
        foreach ($this->variantSourceWidths as $variantSourceWidth) {
519
            $match = false;
520
            switch ($comparison) {
521
                case 'width':
522
                    if ($variantSourceWidth == $width) {
523
                        $match = true;
524
                    }
525
                    break;
526
527
                case 'minwidth':
528
                    if ($variantSourceWidth >= $width) {
529
                        $match = true;
530
                    }
531
                    break;
532
533
                case 'maxwidth':
534
                    if ($variantSourceWidth <= $width) {
535
                        $match = true;
536
                    }
537
                    break;
538
            }
539
            if ($match) {
540
                $subset += array_slice($set, $index, 1, true);
541
            }
542
            $index++;
543
        }
544
545
        return $subset;
546
    }
547
548
    /**
549
     * @param array $array
550
     * @param bool  $dpr
551
     *
552
     * @return string
553
     */
554
    protected function getSrcsetFromArray(array $array, bool $dpr = false): string
555
    {
556
        $srcset = '';
557
        foreach ($array as $key => $value) {
558
            if ($dpr) {
559
                $descriptor = '1x';
560
                if (!empty($array[(int)$key / 2])) {
561
                    $descriptor = '2x';
562
                }
563
                if (!empty($array[(int)$key / 3])) {
564
                    $descriptor = '3x';
565
                }
566
            } else {
567
                $descriptor = $key.'w';
568
            }
569
            $srcset .= $value.' '.$descriptor.', ';
570
        }
571
        $srcset = rtrim($srcset, ', ');
572
573
        return $srcset;
574
    }
575
576
    /**
577
     * Return a default placeholder image
578
     *
579
     * @return \Twig_Markup
580
     */
581
    protected function defaultPlaceholderImage(): \Twig_Markup
582
    {
583
        $width = 1;
584
        $height = 1;
585
        $color = '#CCC';
586
587
        return Template::raw(ImageOptimize::$plugin->placeholder->generatePlaceholderBox($width, $height, $color));
588
    }
589
}
590