Passed
Push — 8.x-dev ( cc9f62...0f16ca )
by Arnaud
03:35
created

Asset::resize()   B

Complexity

Conditions 11
Paths 21

Size

Total Lines 63
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 12.8905

Importance

Changes 4
Bugs 3 Features 0
Metric Value
cc 11
eloc 42
c 4
b 3
f 0
nc 21
nop 1
dl 0
loc 63
ccs 30
cts 40
cp 0.75
crap 12.8905
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Assets\Image\Optimizer;
17
use Cecil\Builder;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Config;
20
use Cecil\Exception\RuntimeException;
21
use Cecil\Util;
22
use Intervention\Image\ImageManagerStatic as ImageManager;
23
use MatthiasMullie\Minify;
24
use ScssPhp\ScssPhp\Compiler;
25
use wapmorgan\Mp3Info\Mp3Info;
26
27
class Asset implements \ArrayAccess
28
{
29
    /** @var Builder */
30
    protected $builder;
31
32
    /** @var Config */
33
    protected $config;
34
35
    /** @var array */
36
    protected $data = [];
37
38
    /** @var bool */
39
    protected $fingerprinted = false;
40
41
    /** @var bool */
42
    protected $compiled = false;
43
44
    /** @var bool */
45
    protected $minified = false;
46
47
    /** @var bool */
48
    protected $optimize = false;
49
    /** @var bool */
50
    protected $optimized = false;
51
52
    /** @var bool */
53
    protected $ignore_missing = false;
54
55
    /**
56
     * Creates an Asset from a file path, an array of files path or an URL.
57
     *
58
     * @param Builder      $builder
59
     * @param string|array $paths
60
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
61
     *
62
     * @throws RuntimeException
63
     */
64 1
    public function __construct(Builder $builder, $paths, array $options = null)
65
    {
66 1
        $this->builder = $builder;
67 1
        $this->config = $builder->getConfig();
68 1
        $paths = is_array($paths) ? $paths : [$paths];
69 1
        array_walk($paths, function ($path) {
70 1
            if (!is_string($path)) {
71
                throw new RuntimeException(\sprintf('The path to an asset must be a string (%s given).', gettype($path)));
72
            }
73 1
            if (empty($path)) {
74
                throw new RuntimeException('The path to an asset can\'t be empty.');
75
            }
76 1
            if (substr($path, 0, 2) == '..') {
77
                throw new RuntimeException(\sprintf('The path to asset "%s" is wrong: it must be directly relative to "assets" or "static" directory, or a remote URL.', $path));
78
            }
79 1
        });
80 1
        $this->data = [
81 1
            'file'           => '',    // absolute file path
82 1
            'files'          => [],    // array of files path (if bundle)
83 1
            'filename'       => '',    // filename
84 1
            'path_source'    => '',    // public path to the file, before transformations
85 1
            'path'           => '',    // public path to the file, after transformations
86 1
            'url'            => null,  // URL of a remote image
87 1
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
88 1
            'ext'            => '',    // file extension
89 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
90 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
91 1
            'size'           => 0,     // file size (in bytes)
92 1
            'content_source' => '',    // file content, before transformations
93 1
            'content'        => '',    // file content, after transformations
94 1
            'width'          => 0,     // width (in pixels) in case of an image
95 1
            'height'         => 0,     // height (in pixels) in case of an image
96 1
            'exif'           => [],    // exif data
97 1
        ];
98
99
        // handles options
100 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
101 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
102 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
103 1
        $filename = '';
104 1
        $ignore_missing = false;
105 1
        $remote_fallback = null;
106 1
        $force_slash = true;
107 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
108 1
        $this->ignore_missing = $ignore_missing;
109
110
        // fill data array with file(s) informations
111 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
112 1
        $cacheKey = \sprintf('%s__%s', implode('_', $paths), $this->builder->getVersion());
113 1
        if (!$cache->has($cacheKey)) {
114 1
            $pathsCount = count($paths);
115 1
            $file = [];
116 1
            for ($i = 0; $i < $pathsCount; $i++) {
117
                // loads file(s)
118 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing, $remote_fallback, $force_slash);
119
                // bundle: same type/ext only
120 1
                if ($i > 0) {
121 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
122
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
123
                    }
124 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
125
                        throw new RuntimeException(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
126
                    }
127
                }
128
                // missing allowed = empty path
129 1
                if ($file[$i]['missing']) {
130 1
                    $this->data['missing'] = true;
131 1
                    $this->data['path'] = $file[$i]['path'];
132
133 1
                    continue;
134
                }
135
                // set data
136 1
                $this->data['size'] += $file[$i]['size'];
137 1
                $this->data['content_source'] .= $file[$i]['content'];
138 1
                $this->data['content'] .= $file[$i]['content'];
139 1
                if ($i == 0) {
140 1
                    $this->data['file'] = $file[$i]['filepath'];
141 1
                    $this->data['filename'] = $file[$i]['path'];
142 1
                    $this->data['path_source'] = $file[$i]['path'];
143 1
                    $this->data['path'] = $file[$i]['path'];
144 1
                    if (!empty($filename)) { /** @phpstan-ignore-line */
145 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
146
                    }
147 1
                    $this->data['url'] = $file[$i]['url'];
148 1
                    $this->data['ext'] = $file[$i]['ext'];
149 1
                    $this->data['type'] = $file[$i]['type'];
150 1
                    $this->data['subtype'] = $file[$i]['subtype'];
151 1
                    if ($this->data['type'] == 'image') {
152 1
                        $this->data['width'] = $this->getWidth();
153 1
                        $this->data['height'] = $this->getHeight();
154 1
                        if ($this->data['subtype'] == 'jpeg') {
155
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
156
                        }
157
                    }
158
                }
159
                // bundle files path
160 1
                $this->data['files'][] = $file[$i]['filepath'];
161
            }
162
            // bundle: define path
163 1
            if ($pathsCount > 1 && empty($filename)) { /** @phpstan-ignore-line */
164
                switch ($this->data['ext']) {
165
                    case 'scss':
166
                    case 'css':
167
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
168
                        break;
169
                    case 'js':
170
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
171
                        break;
172
                    default:
173
                        throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
174
                }
175
            }
176 1
            $cache->set($cacheKey, $this->data);
177
        }
178 1
        $this->data = $cache->get($cacheKey);
179
180
        // fingerprinting
181 1
        if ($fingerprint) {
182 1
            $this->fingerprint();
183
        }
184
        // compiling (Sass files)
185 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
186 1
            $this->compile();
187
        }
188
        // minifying (CSS and JavScript files)
189 1
        if ($minify) {
190 1
            $this->minify();
191
        }
192
        // optimizing (images files)
193 1
        if ($optimize) {
194 1
            $this->optimize = true;
195
        }
196
    }
197
198
    /**
199
     * Returns path.
200
     *
201
     * @throws RuntimeException
202
     */
203 1
    public function __toString(): string
204
    {
205
        try {
206 1
            $this->save();
207
        } catch (\Exception $e) {
208
            $this->builder->getLogger()->error($e->getMessage());
209
        }
210
211 1
        if ($this->isImageInCdn()) {
212
            return $this->buildImageCdnUrl();
213
        }
214
215 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
216
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
217
        }
218
219 1
        return $this->data['path'];
220
    }
221
222
    /**
223
     * Fingerprints a file.
224
     */
225 1
    public function fingerprint(): self
226
    {
227 1
        if ($this->fingerprinted) {
228 1
            return $this;
229
        }
230
231 1
        $fingerprint = hash('md5', $this->data['content_source']);
232 1
        $this->data['path'] = preg_replace(
233 1
            '/\.' . $this->data['ext'] . '$/m',
234 1
            ".$fingerprint." . $this->data['ext'],
235 1
            $this->data['path']
236 1
        );
237
238 1
        $this->fingerprinted = true;
239
240 1
        return $this;
241
    }
242
243
    /**
244
     * Compiles a SCSS.
245
     *
246
     * @throws RuntimeException
247
     */
248 1
    public function compile(): self
249
    {
250 1
        if ($this->compiled) {
251 1
            return $this;
252
        }
253
254 1
        if ($this->data['ext'] != 'scss') {
255 1
            return $this;
256
        }
257
258 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
259 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
260 1
        if (!$cache->has($cacheKey)) {
261 1
            $scssPhp = new Compiler();
262 1
            $importDir = [];
263 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
264 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
265 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
266 1
            $themes = $this->config->getTheme() ?? [];
267 1
            foreach ($scssDir as $dir) {
268 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
269 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
270 1
                $importDir[] = Util::joinPath(dirname($this->data['file']), $dir);
271 1
                foreach ($themes as $theme) {
272 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
273 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
274
                }
275
            }
276 1
            $scssPhp->setImportPaths(array_unique($importDir));
277
            // source map
278 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
279
                $importDir = [];
280
                $assetDir = (string) $this->config->get('assets.dir');
281
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
282
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
283
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
284
                $importDir[] = dirname($filePath);
285
                foreach ($scssDir as $dir) {
286
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
287
                }
288
                $scssPhp->setImportPaths(array_unique($importDir));
289
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
290
                $scssPhp->setSourceMapOptions([
291
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
292
                    'sourceRoot'        => '/',
293
                ]);
294
            }
295
            // output style
296 1
            $outputStyles = ['expanded', 'compressed'];
297 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
298 1
            if (!in_array($outputStyle, $outputStyles)) {
299
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
300
            }
301 1
            $scssPhp->setOutputStyle($outputStyle);
302
            // variables
303 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
304 1
            if (!empty($variables)) {
305 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
306 1
                $scssPhp->replaceVariables($variables);
307
            }
308
            // update data
309 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
310 1
            $this->data['ext'] = 'css';
311 1
            $this->data['type'] = 'text';
312 1
            $this->data['subtype'] = 'text/css';
313 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
314 1
            $this->data['size'] = strlen($this->data['content']);
315 1
            $this->compiled = true;
316 1
            $cache->set($cacheKey, $this->data);
317
        }
318 1
        $this->data = $cache->get($cacheKey);
319
320 1
        return $this;
321
    }
322
323
    /**
324
     * Minifying a CSS or a JS.
325
     *
326
     * @throws RuntimeException
327
     */
328 1
    public function minify(): self
329
    {
330
        // disable minify to preserve inline source map
331 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
332
            return $this;
333
        }
334
335 1
        if ($this->minified) {
336 1
            return $this;
337
        }
338
339 1
        if ($this->data['ext'] == 'scss') {
340
            $this->compile();
341
        }
342
343 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
344 1
            return $this;
345
        }
346
347 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
348 1
            $this->minified;
349
350 1
            return $this;
351
        }
352
353 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
354 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
355 1
        if (!$cache->has($cacheKey)) {
356 1
            switch ($this->data['ext']) {
357 1
                case 'css':
358 1
                    $minifier = new Minify\CSS($this->data['content']);
359 1
                    break;
360 1
                case 'js':
361 1
                    $minifier = new Minify\JS($this->data['content']);
362 1
                    break;
363
                default:
364
                    throw new RuntimeException(\sprintf('Not able to minify "%s"', $this->data['path']));
365
            }
366 1
            $this->data['path'] = preg_replace(
367 1
                '/\.' . $this->data['ext'] . '$/m',
368 1
                '.min.' . $this->data['ext'],
369 1
                $this->data['path']
370 1
            );
371 1
            $this->data['content'] = $minifier->minify();
372 1
            $this->data['size'] = strlen($this->data['content']);
373 1
            $this->minified = true;
374 1
            $cache->set($cacheKey, $this->data);
375
        }
376 1
        $this->data = $cache->get($cacheKey);
377
378 1
        return $this;
379
    }
380
381
    /**
382
     * Optimizing an image.
383
     */
384 1
    public function optimize(string $filepath): self
385
    {
386 1
        if ($this->data['type'] != 'image') {
387 1
            return $this;
388
        }
389
390 1
        $quality = $this->config->get('assets.images.quality') ?? 75;
391 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
392 1
        $tags = ["q$quality", 'optimized'];
393 1
        if ($this->data['width']) {
394 1
            array_unshift($tags, "{$this->data['width']}x");
395
        }
396 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
397 1
        if (!$cache->has($cacheKey)) {
398 1
            $message = $this->data['path'];
399 1
            $sizeBefore = filesize($filepath);
400 1
            try {
401 1
                Optimizer::create($quality)->optimize($filepath);
402 1
            } catch (\Exception $e) {
403
                $this->builder->getLogger()->error(\sprintf('Can\'t optimize image "%s": "%s"', $filepath, $e->getMessage()));
404
            }
405
            $sizeAfter = filesize($filepath);
406
            if ($sizeAfter < $sizeBefore) {
407
                $message = \sprintf(
408
                    '%s (%s Ko -> %s Ko)',
409
                    $message,
410 1
                    ceil($sizeBefore / 1000),
411 1
                    ceil($sizeAfter / 1000)
412 1
                );
413 1
            }
414
            $this->data['content'] = Util\File::fileGetContents($filepath);
415 1
            $this->data['size'] = $sizeAfter;
416 1
            $cache->set($cacheKey, $this->data);
417
            $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
418 1
        }
419
        $this->data = $cache->get($cacheKey);
420
        $this->optimized = true;
421
422
        return $this;
423
    }
424
425
    /**
426 1
     * Resizes an image with a new $width.
427
     *
428 1
     * @throws RuntimeException
429
     */
430
    public function resize(int $width): self
431 1
    {
432
        if ($this->data['missing']) {
433
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
434 1
        }
435
        if ($this->data['type'] != 'image') {
436
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
437
        }
438 1
        if ($width >= $this->data['width']) {
439 1
            return $this;
440
        }
441 1
442
        $assetResized = clone $this;
443
        $assetResized->data['width'] = $width;
444
445 1
        if ($this->isImageInCdn()) {
446 1
            return $assetResized; // returns the asset with the new width only: CDN do the rest of the job
447 1
        }
448 1
449 1
        $quality = $this->config->get('assets.images.quality');
450
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
451
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
452 1
        if (!$cache->has($cacheKey)) {
453
            if ($assetResized->data['type'] !== 'image') {
454
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
455
            }
456
            if (!extension_loaded('gd')) {
457 1
                throw new RuntimeException('GD extension is required to use images resize.');
458 1
            }
459 1
460 1
            try {
461 1
                $img = ImageManager::make($assetResized->data['content_source'])->encode($assetResized->data['ext']);
462
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
463
                    $constraint->aspectRatio();
464
                    $constraint->upsize();
465 1
                });
466 1
            } catch (\Exception $e) {
467 1
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
468 1
            }
469 1
            $assetResized->data['path'] = '/' . Util::joinPath(
470 1
                (string) $this->config->get('assets.target'),
471
                (string) $this->config->get('assets.images.resize.dir'),
472
                (string) $width,
473 1
                $assetResized->data['path']
474
            );
475
476 1
            try {
477 1
                if ($assetResized->data['subtype'] == 'image/jpeg') {
478 1
                    $img->interlace();
479 1
                }
480
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
481
                $img->destroy();
482
                $assetResized->data['height'] = $assetResized->getHeight();
483
                $assetResized->data['size'] = strlen($assetResized->data['content']);
484 1
            } catch (\Exception $e) {
485
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
486 1
            }
487
488 1
            $cache->set($cacheKey, $assetResized->data);
489
        }
490
        $assetResized->data = $cache->get($cacheKey);
491
492
        return $assetResized;
493
    }
494
495
    /**
496 1
     * Converts an image asset to WebP format.
497
     *
498 1
     * @throws RuntimeException
499
     */
500
    public function webp(?int $quality = null): self
501
    {
502 1
        if ($this->data['type'] !== 'image') {
503 1
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
504
        }
505
506 1
        if ($quality === null) {
507 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
508 1
        }
509
510 1
        $assetWebp = clone $this;
511
        $format = 'webp';
512
        $assetWebp['ext'] = $format;
513
514 1
        if ($this->isImageInCdn()) {
515 1
            return $assetWebp; // returns the asset with the new extension ('webp') only: CDN do the rest of the job
516
        }
517
518
        $img = ImageManager::make($assetWebp['content']);
519
        $assetWebp['content'] = (string) $img->encode($format, $quality);
520
        $img->destroy();
521
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
522
        $assetWebp['subtype'] = "image/$format";
523
        $assetWebp['size'] = strlen((string) $assetWebp['content']);
524
525
        return $assetWebp;
526
    }
527
528 1
    /**
529
     * Implements \ArrayAccess.
530 1
     */
531 1
    #[\ReturnTypeWillChange]
532
    public function offsetSet($offset, $value): void
533
    {
534
        if (!is_null($offset)) {
535
            $this->data[$offset] = $value;
536
        }
537
    }
538
539 1
    /**
540
     * Implements \ArrayAccess.
541 1
     */
542
    #[\ReturnTypeWillChange]
543
    public function offsetExists($offset): bool
544
    {
545
        return isset($this->data[$offset]);
546
    }
547
548
    /**
549
     * Implements \ArrayAccess.
550
     */
551
    #[\ReturnTypeWillChange]
552
    public function offsetUnset($offset): void
553
    {
554
        unset($this->data[$offset]);
555
    }
556
557 1
    /**
558
     * Implements \ArrayAccess.
559 1
     */
560
    #[\ReturnTypeWillChange]
561
    public function offsetGet($offset)
562
    {
563
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
564
    }
565
566
    /**
567
     * Hashing content of an asset with the specified algo, sha384 by default.
568 1
     * Used for SRI (Subresource Integrity).
569
     *
570 1
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
571
     */
572
    public function getIntegrity(string $algo = 'sha384'): string
573
    {
574
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
575
    }
576
577
    /**
578
     * Returns MP3 file infos.
579
     *
580
     * @see https://github.com/wapmorgan/Mp3Info
581
     */
582
    public function getAudio(): Mp3Info
583
    {
584
        if ($this->data['type'] !== 'audio') {
585
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
586
        }
587
588
        return new Mp3Info($this->data['file']);
589
    }
590
591
    /**
592 1
     * Returns the data URL (encoded in Base64).
593
     *
594 1
     * @throws RuntimeException
595 1
     */
596
    public function dataurl(): string
597
    {
598 1
        if ($this->data['type'] == 'image' && !$this->isSVG()) {
599
            return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
600
        }
601
602
        return sprintf("data:%s;base64,%s", $this->data['subtype'], base64_encode($this->data['content']));
603
    }
604
605
    /**
606
     * Saves file.
607 1
     * Note: a file from `static/` with the same name will NOT be overridden.
608
     *
609 1
     * @throws RuntimeException
610 1
     */
611
    public function save(): void
612 1
    {
613 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
614 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
615 1
            try {
616
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
617
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $filepath));
618
                if ($this->optimize) {
619
                    $this->optimize($filepath);
620
                }
621
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
622
                if (!$this->ignore_missing) {
623
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
624
                }
625
            }
626
        }
627
    }
628
629
    /**
630 1
     * Is Asset is an image in CDN.
631
     *
632 1
     * @return boolean
633 1
     */
634
    public function isImageInCdn()
635
    {
636
        if ($this->data['type'] != 'image' || $this->isSVG() || (bool) $this->config->get('assets.images.cdn.enabled') !== true) {
637
            return false;
638
        }
639
        // remote image?
640
        if ($this->data['url'] !== null && (bool) $this->config->get('assets.images.cdn.remote') !== true) {
641
            return false;
642
        }
643
644
        return true;
645
    }
646
647
    /**
648 1
     * Load file data.
649
     *
650 1
     * @throws RuntimeException
651 1
     */
652 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
653
    {
654 1
        $file = [
655 1
            'url' => null,
656 1
        ];
657 1
658
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
659 1
            if ($ignore_missing) {
660
                $file['path'] = $path;
661
                $file['missing'] = true;
662 1
663
                return $file;
664
            }
665 1
666 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
667 1
        }
668 1
669 1
        if (Util\Url::isUrl($path)) {
670
            $file['url'] = $path;
671 1
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
672 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
673
            $force_slash = true;
674
        }
675 1
        if ($force_slash) {
676 1
            $path = '/' . ltrim($path, '/');
677
        }
678 1
679 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
680 1
        $content = Util\File::fileGetContents($filePath);
681 1
682 1
        $file['filepath'] = $filePath;
683 1
        $file['path'] = $path;
684 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
685 1
        $file['type'] = $type;
686
        $file['subtype'] = $subtype;
687 1
        $file['size'] = filesize($filePath);
688
        $file['content'] = $content;
689
        $file['missing'] = false;
690
691
        return $file;
692
    }
693
694
    /**
695
     * Try to find the file:
696
     *   1. remote (if $path is a valid URL)
697
     *   2. in static/
698
     *   3. in themes/<theme>/static/
699
     * Returns local file path or false if file don't exists.
700
     *
701 1
     * @throws RuntimeException
702
     *
703
     * @return string|false
704 1
     */
705 1
    private function findFile(string $path, ?string $remote_fallback = null)
706 1
    {
707 1
        // in case of remote file: save it and returns cached file path
708 1
        if (Util\Url::isUrl($path)) {
709 1
            $url = $path;
710
            $urlHost = parse_url($path, PHP_URL_HOST);
711 1
            $urlPath = parse_url($path, PHP_URL_PATH);
712 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
713
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
714 1
            // Google Fonts hack
715 1
            if (strpos($urlPath, '/css') !== false) {
716 1
                $extension = 'css';
717 1
            }
718 1
            $relativePath = Page::slugify(\sprintf(
719 1
                '%s%s%s%s',
720 1
                $urlHost,
721 1
                $this->sanitize($urlPath),
722 1
                $urlQuery ? "-$urlQuery" : '',
723 1
                $urlQuery && $extension ? ".$extension" : ''
724
            ));
725 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
726 1
            if (!file_exists($filePath)) {
727 1
                if (!Util\Url::isRemoteFileExists($url)) {
728
                    // is there a fallback in assets/
729
                    if ($remote_fallback) {
730
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
731
                        if (Util\File::getFS()->exists($filePath)) {
732 1
                            return $filePath;
733
                        }
734 1
                    }
735
736
                    return false;
737 1
                }
738
                if (false === $content = Util\File::fileGetContents($url, true)) {
739
                    return false;
740 1
                }
741
                if (strlen($content) <= 1) {
742
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
743 1
                }
744
                Util\File::getFS()->dumpFile($filePath, $content);
745
            }
746
747 1
            return $filePath;
748 1
        }
749 1
750
        // checks in assets/
751
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
752
        if (Util\File::getFS()->exists($filePath)) {
753 1
            return $filePath;
754 1
        }
755 1
756
        // checks in each themes/<theme>/assets/
757
        foreach ($this->config->getTheme() as $theme) {
758
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
759
            if (Util\File::getFS()->exists($filePath)) {
760
                return $filePath;
761 1
            }
762 1
        }
763 1
764
        // checks in static/
765
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
766
        if (Util\File::getFS()->exists($filePath)) {
767 1
            return $filePath;
768 1
        }
769 1
770 1
        // checks in each themes/<theme>/static/
771
        foreach ($this->config->getTheme() as $theme) {
772
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
773
            if (Util\File::getFS()->exists($filePath)) {
774 1
                return $filePath;
775
            }
776
        }
777
778
        return false;
779
    }
780
781
    /**
782 1
     * Returns the width of an image/SVG.
783
     *
784 1
     * @throws RuntimeException
785
     */
786
    private function getWidth(): int
787 1
    {
788 1
        if ($this->data['type'] != 'image') {
789
            return 0;
790 1
        }
791
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
792
            return (int) $svg->width;
793
        }
794 1
        if (false === $size = $this->getImageSize()) {
795
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
796
        }
797
798
        return $size[0];
799
    }
800
801
    /**
802 1
     * Returns the height of an image/SVG.
803
     *
804 1
     * @throws RuntimeException
805
     */
806
    private function getHeight(): int
807 1
    {
808 1
        if ($this->data['type'] != 'image') {
809
            return 0;
810 1
        }
811
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
812
            return (int) $svg->height;
813
        }
814 1
        if (false === $size = $this->getImageSize()) {
815
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
816
        }
817
818
        return $size[1];
819
    }
820
821
    /**
822
     * Returns image size informations.
823
     *
824 1
     * @see https://www.php.net/manual/function.getimagesize.php
825
     *
826 1
     * @return array|false
827
     */
828
    private function getImageSize()
829
    {
830
        if (!$this->data['type'] == 'image') {
831 1
            return false;
832 1
        }
833
834
        try {
835
            if (false === $size = getimagesizefromstring($this->data['content'])) {
836
                return false;
837
            }
838 1
        } catch (\Exception $e) {
839
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
840
        }
841
842
        return $size;
843
    }
844 1
845
    /**
846 1
     * Returns true if asset is a SVG.
847
     */
848
    private function isSVG(): bool
849
    {
850
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
851
    }
852
853
    /**
854 1
     * Returns SVG attributes.
855
     *
856 1
     * @return \SimpleXMLElement|false
857
     */
858
    private function getSvgAttributes()
859
    {
860 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
861
            return false;
862
        }
863
864
        return $xml->attributes();
865
    }
866 1
867
    /**
868 1
     * Replaces some characters by '_'.
869
     */
870
    private function sanitize(string $string): string
871
    {
872
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
873
    }
874
875
    /**
876
     * Builds CDN image URL.
877
     */
878
    private function buildImageCdnUrl(): string
879
    {
880
        return str_replace(
881
            [
882
                '%account%',
883
                '%image_url%',
884
                '%width%',
885
                '%quality%',
886
                '%format%',
887
            ],
888
            [
889
                $this->config->get('assets.images.cdn.account'),
890
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
891
                $this->data['width'],
892
                $this->config->get('assets.images.quality') ?? 75,
893
                $this->data['ext'],
894
            ],
895
            (string) $this->config->get('assets.images.cdn.url')
896
        );
897
    }
898
}
899