├── LICENSE ├── README.md ├── classes └── ThumbHash.php ├── composer.json ├── index.php └── vendor ├── autoload.php ├── composer ├── ClassLoader.php ├── InstalledVersions.php ├── autoload_classmap.php ├── autoload_files.php ├── autoload_namespaces.php ├── autoload_psr4.php ├── autoload_real.php ├── autoload_static.php ├── installed.php └── platform_check.php └── srwiez └── thumbhash └── src ├── Thumbhash.php └── helpers.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tobias Möritz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Kirby ThumbHash Banner](./.github/gh-banner.png) 2 | 3 | # Kirby ThumbHash 4 | 5 | [ThumbHash](https://evanw.github.io/thumbhash/) is an alternative image placeholder algorithm. 6 | Placeholders are represented by small ∼28 bytes hashes. It's similar to [BlurHash](https://blurha.sh/) but with the following advantages: 7 | 8 | - Encodes more detail in the same space 9 | - Also encodes the aspect ratio 10 | - Gives more accurate colors 11 | - Supports images with alpha 12 | 13 | This plugin adds ThumbHash support to Kirby, allowing you to implement UX improvements such as progressive image loading or content-aware spoiler images [like Mastodon](https://blog.joinmastodon.org/2019/05/improving-support-for-adult-content-on-mastodon/). 14 | 15 | If you rather want to use BlurHash in your application, feel free to use my [kirby-blurhash](https://github.com/tobimori/kirby-blurhash) plugin. 16 | 17 | Under the hood, the heavy work gets done by a PHP implementation of ThumbHash by [SRWieZ](https://github.com/SRWieZ): [SRWieZ/thumbhash](https://github.com/SRWieZ/thumbhash) 18 | 19 | ## Requirements 20 | 21 | - Kirby 3.9.2+ for asset methods 22 | - PHP 8.0+ 23 | - `gd` extension 24 | 25 | ## Installation 26 | 27 | ### Download 28 | 29 | Download and copy this repository to `/site/plugins/kirby-thumbhash`. 30 | 31 | ### Composer 32 | 33 | ``` 34 | composer require tobimori/kirby-thumbhash 35 | ``` 36 | 37 | ## Usage 38 | 39 | ### Client-side decoding 40 | 41 | #### **`$file->thumbhash()`** 42 | 43 | > Encodes the image with ThumbHash and returns ThumbHash as a string 44 | 45 | The default implementation of ThumbHash expects the string to be decoded on the client-side. 46 | 47 | This provides the most benefits, most notably including better color representation and smaller payload size, but requires the initial execution of such a library on the client-side, and thus is better used with a headless site or heavily makes use of client-side infinite scrolling/loading. 48 | 49 | With an lazy-loading library like [unlazy](https://unlazy.byjohann.dev/) you can implement lazy-loading with client-side decoding easily by providing the thumbhash as attribute. 50 | 51 | ```php 52 | <?= $image->alt() ?> 58 | ``` 59 | 60 | ### Server-side decoding 61 | 62 | #### **`$file->thumbhashUri()`** 63 | 64 | > Encodes the image with ThumbHash, then decodes & rasterizes it. Finally returns it as a data URI which can be used without any client-side library. 65 | 66 | In addition to simply outputting the ThumbHash string for usage on the client-side, this plugin also provides a server-side decoding option that allows you to output a base64-encoded image string, which can be used as a placeholder image without any client-side libraries, similar to [Kirby Blurry Placeholder](https://github.com/johannschopplich/kirby-blurry-placeholder). 67 | 68 | This is especially useful when you only have a few images on your site or don't want to go through the hassle of using a client-side library for outputting placeholders. Using this approach, you'll still get better color representation of the ThumbHash algorithm than with regularly downsizing an image, but image previews will still be about ~1kB large. 69 | 70 | ```php 71 | 72 | ``` 73 | 74 | ### Cropped images 75 | 76 | Kirby doesn't support file methods on cropped images, so you'll have to use the original image, and pass the ratio as attribute to the element to get the correct ThumbHash. 77 | 78 | ```php 79 | crop(500, 400) ?> 80 | <?= $original->alt() ?> 86 | ``` 87 | 88 | This is also supported by `$file->thumbhash($ratio)`. 89 | 90 | ### Working with static assets (using `asset()` helper) 91 | 92 | All methods are available as asset methods since Kirby 3.9.2. 93 | 94 | ```php 95 | asset('assets/image.jpg')->thumbhash(); 96 | asset('assets/image.jpg')->thumbhashUri(); 97 | ``` 98 | 99 | [Read more about the `asset()` helper here](https://getkirby.com/docs/reference/objects/filesystem/asset). 100 | 101 | ### Aliases 102 | 103 | ```php 104 | $file->th(); // thumbhash() 105 | $file->thUri(); // thumbhashUri() 106 | ``` 107 | 108 | ### Options 109 | 110 | Each method also allows you to specify the ratio and blur radius as options array. 111 | 112 | ```php 113 | $file->thumbhash([ 'ratio' => 16/9 ]); // will return thumbhash string, cropped to 16:9 114 | $file->thumbhashUri([ 'blur' => 2 ]); // will return placeholder, encoded in an svg with blur filter 115 | $file->thumbhashUri([ 'ratio' => 3/2, 'blur' => 0 ]); // will return placeholder as base64-encoded png without filter, cropped to 3:2 116 | ``` 117 | 118 | ### Clear cache 119 | 120 | The encoding cache is automatically cleared when an image gets replaced or updated, however you can also clear the cache manually with the `clearCache` static method: 121 | 122 | ```php 123 | [ 146 | 'sampleMaxSize' => 100, 147 | 'blurRadius' => 1, 148 | ], 149 | ]; 150 | ``` 151 | 152 | ## Credits 153 | 154 | - Johann Schopplich's [Kirby Blurry Placeholder](https://github.com/johannschopplich/kirby-blurry-placeholder) plugin that set the baseline for this plugin (Licensed under [MIT License](https://github.com/johannschopplich/kirby-blurry-placeholder/blob/main/LICENSE) - Copyright © 2020-2022 Johann Schopplich) 155 | 156 | ## License 157 | 158 | [MIT License](./LICENSE) 159 | Copyright © 2023 Tobias Möritz 160 | -------------------------------------------------------------------------------- /classes/ThumbHash.php: -------------------------------------------------------------------------------- 1 | ratio(); 31 | $cache = $kirby->cache('tobimori.thumbhash.encode'); 32 | 33 | if (($cacheData = $cache->get($id)) !== null) { 34 | return $cacheData; 35 | } 36 | 37 | // Generate a sample image for encode to avoid memory issues. 38 | $max = $kirby->option('tobimori.thumbhash.sampleMaxSize'); // Max width or height 39 | 40 | $height = round($file->height() > $file->width() ? $max : $max / $options['ratio']); 41 | $width = round($file->width() > $file->height() ? $max : $max * $options['ratio']); 42 | $options = [ 43 | 'width' => $width, 44 | 'height' => $height, 45 | 'crop' => true, 46 | 'quality' => 70, 47 | ]; 48 | 49 | // Create a GD image from the file. 50 | $image = imagecreatefromstring($file->thumb($options)->read()); // TODO: allow Imagick encoder 51 | $height = imagesy($image); 52 | $width = imagesx($image); 53 | $pixels = []; 54 | 55 | for ($y = 0; $y < $height; $y++) { 56 | for ($x = 0; $x < $width; $x++) { 57 | $color_index = imagecolorat($image, $x, $y); 58 | $color = imagecolorsforindex($image, $color_index); 59 | $alpha = 255 - ceil($color['alpha'] * (255 / 127)); // GD only supports 7-bit alpha channel 60 | $pixels[] = $color['red']; 61 | $pixels[] = $color['green']; 62 | $pixels[] = $color['blue']; 63 | $pixels[] = $alpha; 64 | } 65 | } 66 | 67 | $hashArray = THEncoder::RGBAToHash($width, $height, $pixels); 68 | $thumbhash = THEncoder::convertHashToString($hashArray); 69 | $cache->set($id, $thumbhash); 70 | 71 | return $thumbhash; 72 | } 73 | 74 | /** 75 | * Decodes a ThumbHash string or array to an array of RGBA values, and width & height 76 | */ 77 | public static function decode(string|array $thumbhash): array 78 | { 79 | $kirby = kirby(); 80 | $cache = $kirby->cache('tobimori.thumbhash.decode'); 81 | 82 | $id = is_array($thumbhash) ? THEncoder::convertHashToString($thumbhash) : $thumbhash; 83 | $thumbhash = is_string($thumbhash) ? THEncoder::convertStringToHash($thumbhash) : $thumbhash; 84 | 85 | if (($cacheData = $cache->get($id)) !== null) { 86 | return $cacheData; 87 | } 88 | 89 | $image = THEncoder::hashToRGBA($thumbhash); 90 | // check if any alpha value in RGBA array is less than 255 91 | $transparent = array_reduce(array_chunk($image['rgba'], 4), function ($carry, $item) { 92 | return $carry || $item[3] < 255; 93 | }, false); 94 | 95 | $dataUri = THEncoder::rgbaToDataURL($image['w'], $image['h'], $image['rgba']); 96 | 97 | $data = [ 98 | 'uri' => $dataUri, 99 | 'width' => $image['w'], 100 | 'height' => $image['h'], 101 | 'transparent' => $transparent, 102 | ]; 103 | 104 | $cache->set($id, $data); 105 | return $data; 106 | } 107 | 108 | /** 109 | * Clears encoding cache for a file. 110 | */ 111 | public static function clearCache(Asset|File $file) 112 | { 113 | $cache = kirby()->cache('tobimori.thumbhash.encode'); 114 | $id = self::getId($file); 115 | $cache->remove($id); 116 | } 117 | 118 | /** 119 | * Returns an optimized URI-encoded string of an SVG for using in a src attribute. 120 | * Based on https://github.com/johannschopplich/kirby-blurry-placeholder/blob/main/BlurryPlaceholder.php#L65 121 | */ 122 | private static function svgToUri(string $data): string 123 | { 124 | // Optimizes the data URI length by deleting line breaks and 125 | // removing unnecessary spaces 126 | $data = preg_replace('/\s+/', ' ', $data); 127 | $data = preg_replace('/> <', $data); 128 | 129 | $data = rawurlencode($data); 130 | 131 | // Back-decode certain characters to improve compression 132 | // except '%20' to be compliant with W3C guidelines 133 | $data = str_replace( 134 | ['%2F', '%3A', '%3D'], 135 | ['/', ':', '='], 136 | $data 137 | ); 138 | 139 | return 'data:image/svg+xml;charset=utf-8,' . $data; 140 | } 141 | 142 | /** 143 | * Applies SVG filter and base64-encoding to binary image. 144 | * Based on https://github.com/johannschopplich/kirby-blurry-placeholder/blob/main/BlurryPlaceholder.php#L10 145 | */ 146 | private static function svgFilter(array $image, array $options = []): string 147 | { 148 | 149 | $svgHeight = number_format($image['height'], 2, '.', ''); 150 | $svgWidth = number_format($image['width'], 2, '.', ''); 151 | 152 | // Wrap the blurred image in a SVG to avoid rasterizing the filter 153 | 154 | $alphaFilter = ''; 155 | 156 | // If the image doesn't include an alpha channel itself, apply an additional filter 157 | // to remove the alpha channel from the blur at the edges 158 | if (!$image['transparent']) { 159 | $alphaFilter = << 161 | 162 | 163 | EOD; 164 | } 165 | // Wrap the blurred image in a SVG to avoid rasterizing the filter 166 | $svg = << 168 | 169 | 170 | {$alphaFilter} 171 | 172 | 173 | 174 | EOD; 175 | 176 | return $svg; 177 | } 178 | 179 | /** 180 | * Returns a decoded BlurHash as a URI-encoded SVG with blur filter applied. 181 | */ 182 | public static function uri(array $image, array $options = []): string 183 | { 184 | $uri = $image['uri']; 185 | $options['blurRadius'] ??= kirby()->option('tobimori.thumbhash.blurRadius') ?? 1; 186 | 187 | if ($options['blurRadius'] !== 0) { 188 | $svg = self::svgFilter($image, $options); 189 | $uri = self::svgToUri($svg); 190 | } 191 | 192 | return $uri; 193 | } 194 | 195 | 196 | /** 197 | * Returns the uuid for a File, or its mediaHash for Assets. 198 | */ 199 | private static function getId(Asset|File $file): string 200 | { 201 | if ($file instanceof Asset) { 202 | return $file->mediaHash(); 203 | } 204 | 205 | return $file->uuid()?->id() ?? $file->id(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tobimori/kirby-thumbhash", 3 | "description": "Kirby Image placeholders with the smarter ThumbHash integration", 4 | "type": "kirby-plugin", 5 | "version": "1.1.1", 6 | "license": "MIT", 7 | "homepage": "https://github.com/tobimori/kirby-thumbhash#readme", 8 | "authors": [ 9 | { 10 | "name": "Tobias Möritz", 11 | "email": "tobias@moeritz.io" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-4": { 16 | "tobimori\\": "classes/" 17 | } 18 | }, 19 | "require": { 20 | "php": ">=8.0.0", 21 | "getkirby/composer-installer": "^1.2", 22 | "srwiez/thumbhash": "^1.1" 23 | }, 24 | "require-dev": { 25 | "getkirby/cms": "^3.8" 26 | }, 27 | "scripts": { 28 | "dist": "composer install --no-dev --optimize-autoloader" 29 | }, 30 | "config": { 31 | "optimize-autoloader": true, 32 | "allow-plugins": { 33 | "getkirby/composer-installer": true 34 | } 35 | }, 36 | "extra": { 37 | "kirby-cms-path": false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | [ 10 | /** @kql-allowed */ 11 | 'thumbhash' => fn (array $options = []) => ThumbHash::encode($this, $options), 12 | /** @kql-allowed */ 13 | 'th' => fn (array $options = []) => $this->thumbhash($options), 14 | /** @kql-allowed */ 15 | 'thumbhashUri' => fn (array $options = []) => ThumbHash::thumb($this, $options), 16 | /** @kql-allowed */ 17 | 'thUri' => fn (array $options = []) => $this->thumbhashUri($options), 18 | ], 19 | 'assetMethods' => [ 20 | /** @kql-allowed */ 21 | 'thumbhash' => fn (array $options = []) => ThumbHash::encode($this, $options), 22 | /** @kql-allowed */ 23 | 'th' => fn (array $options = []) => $this->thumbhash($options), 24 | /** @kql-allowed */ 25 | 'thumbhashUri' => fn (array $options = []) => ThumbHash::thumb($this, $options), 26 | /** @kql-allowed */ 27 | 'thUri' => fn (array $options = []) => $this->thumbhashUri($options), 28 | ], 29 | 'options' => [ 30 | 'cache.encode' => true, 31 | 'cache.decode' => true, 32 | //'engine' => 'gd', // `gd` or `imagick` - TODO 33 | 'blurRadius' => 1, // Blur radius, larger values are smoother, but less accurate 34 | 'sampleMaxSize' => 100, // Max width or height for smaller image that gets encoded (Memory constraints) 35 | ], 36 | 'hooks' => [ 37 | 'file.update:before' => fn ($file) => ThumbHash::clearCache($file), 38 | 'file.replace:before' => fn ($file) => ThumbHash::clearCache($file), 39 | ] 40 | ]); 41 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see https://www.php-fig.org/psr/psr-0/ 41 | * @see https://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | /** @var \Closure(string):void */ 46 | private static $includeFile; 47 | 48 | /** @var ?string */ 49 | private $vendorDir; 50 | 51 | // PSR-4 52 | /** 53 | * @var array[] 54 | * @psalm-var array> 55 | */ 56 | private $prefixLengthsPsr4 = array(); 57 | /** 58 | * @var array[] 59 | * @psalm-var array> 60 | */ 61 | private $prefixDirsPsr4 = array(); 62 | /** 63 | * @var array[] 64 | * @psalm-var array 65 | */ 66 | private $fallbackDirsPsr4 = array(); 67 | 68 | // PSR-0 69 | /** 70 | * @var array[] 71 | * @psalm-var array> 72 | */ 73 | private $prefixesPsr0 = array(); 74 | /** 75 | * @var array[] 76 | * @psalm-var array 77 | */ 78 | private $fallbackDirsPsr0 = array(); 79 | 80 | /** @var bool */ 81 | private $useIncludePath = false; 82 | 83 | /** 84 | * @var string[] 85 | * @psalm-var array 86 | */ 87 | private $classMap = array(); 88 | 89 | /** @var bool */ 90 | private $classMapAuthoritative = false; 91 | 92 | /** 93 | * @var bool[] 94 | * @psalm-var array 95 | */ 96 | private $missingClasses = array(); 97 | 98 | /** @var ?string */ 99 | private $apcuPrefix; 100 | 101 | /** 102 | * @var self[] 103 | */ 104 | private static $registeredLoaders = array(); 105 | 106 | /** 107 | * @param ?string $vendorDir 108 | */ 109 | public function __construct($vendorDir = null) 110 | { 111 | $this->vendorDir = $vendorDir; 112 | self::initializeIncludeClosure(); 113 | } 114 | 115 | /** 116 | * @return string[] 117 | */ 118 | public function getPrefixes() 119 | { 120 | if (!empty($this->prefixesPsr0)) { 121 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 122 | } 123 | 124 | return array(); 125 | } 126 | 127 | /** 128 | * @return array[] 129 | * @psalm-return array> 130 | */ 131 | public function getPrefixesPsr4() 132 | { 133 | return $this->prefixDirsPsr4; 134 | } 135 | 136 | /** 137 | * @return array[] 138 | * @psalm-return array 139 | */ 140 | public function getFallbackDirs() 141 | { 142 | return $this->fallbackDirsPsr0; 143 | } 144 | 145 | /** 146 | * @return array[] 147 | * @psalm-return array 148 | */ 149 | public function getFallbackDirsPsr4() 150 | { 151 | return $this->fallbackDirsPsr4; 152 | } 153 | 154 | /** 155 | * @return string[] Array of classname => path 156 | * @psalm-return array 157 | */ 158 | public function getClassMap() 159 | { 160 | return $this->classMap; 161 | } 162 | 163 | /** 164 | * @param string[] $classMap Class to filename map 165 | * @psalm-param array $classMap 166 | * 167 | * @return void 168 | */ 169 | public function addClassMap(array $classMap) 170 | { 171 | if ($this->classMap) { 172 | $this->classMap = array_merge($this->classMap, $classMap); 173 | } else { 174 | $this->classMap = $classMap; 175 | } 176 | } 177 | 178 | /** 179 | * Registers a set of PSR-0 directories for a given prefix, either 180 | * appending or prepending to the ones previously set for this prefix. 181 | * 182 | * @param string $prefix The prefix 183 | * @param string[]|string $paths The PSR-0 root directories 184 | * @param bool $prepend Whether to prepend the directories 185 | * 186 | * @return void 187 | */ 188 | public function add($prefix, $paths, $prepend = false) 189 | { 190 | if (!$prefix) { 191 | if ($prepend) { 192 | $this->fallbackDirsPsr0 = array_merge( 193 | (array) $paths, 194 | $this->fallbackDirsPsr0 195 | ); 196 | } else { 197 | $this->fallbackDirsPsr0 = array_merge( 198 | $this->fallbackDirsPsr0, 199 | (array) $paths 200 | ); 201 | } 202 | 203 | return; 204 | } 205 | 206 | $first = $prefix[0]; 207 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 208 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 209 | 210 | return; 211 | } 212 | if ($prepend) { 213 | $this->prefixesPsr0[$first][$prefix] = array_merge( 214 | (array) $paths, 215 | $this->prefixesPsr0[$first][$prefix] 216 | ); 217 | } else { 218 | $this->prefixesPsr0[$first][$prefix] = array_merge( 219 | $this->prefixesPsr0[$first][$prefix], 220 | (array) $paths 221 | ); 222 | } 223 | } 224 | 225 | /** 226 | * Registers a set of PSR-4 directories for a given namespace, either 227 | * appending or prepending to the ones previously set for this namespace. 228 | * 229 | * @param string $prefix The prefix/namespace, with trailing '\\' 230 | * @param string[]|string $paths The PSR-4 base directories 231 | * @param bool $prepend Whether to prepend the directories 232 | * 233 | * @throws \InvalidArgumentException 234 | * 235 | * @return void 236 | */ 237 | public function addPsr4($prefix, $paths, $prepend = false) 238 | { 239 | if (!$prefix) { 240 | // Register directories for the root namespace. 241 | if ($prepend) { 242 | $this->fallbackDirsPsr4 = array_merge( 243 | (array) $paths, 244 | $this->fallbackDirsPsr4 245 | ); 246 | } else { 247 | $this->fallbackDirsPsr4 = array_merge( 248 | $this->fallbackDirsPsr4, 249 | (array) $paths 250 | ); 251 | } 252 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 253 | // Register directories for a new namespace. 254 | $length = strlen($prefix); 255 | if ('\\' !== $prefix[$length - 1]) { 256 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 257 | } 258 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 259 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 260 | } elseif ($prepend) { 261 | // Prepend directories for an already registered namespace. 262 | $this->prefixDirsPsr4[$prefix] = array_merge( 263 | (array) $paths, 264 | $this->prefixDirsPsr4[$prefix] 265 | ); 266 | } else { 267 | // Append directories for an already registered namespace. 268 | $this->prefixDirsPsr4[$prefix] = array_merge( 269 | $this->prefixDirsPsr4[$prefix], 270 | (array) $paths 271 | ); 272 | } 273 | } 274 | 275 | /** 276 | * Registers a set of PSR-0 directories for a given prefix, 277 | * replacing any others previously set for this prefix. 278 | * 279 | * @param string $prefix The prefix 280 | * @param string[]|string $paths The PSR-0 base directories 281 | * 282 | * @return void 283 | */ 284 | public function set($prefix, $paths) 285 | { 286 | if (!$prefix) { 287 | $this->fallbackDirsPsr0 = (array) $paths; 288 | } else { 289 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 290 | } 291 | } 292 | 293 | /** 294 | * Registers a set of PSR-4 directories for a given namespace, 295 | * replacing any others previously set for this namespace. 296 | * 297 | * @param string $prefix The prefix/namespace, with trailing '\\' 298 | * @param string[]|string $paths The PSR-4 base directories 299 | * 300 | * @throws \InvalidArgumentException 301 | * 302 | * @return void 303 | */ 304 | public function setPsr4($prefix, $paths) 305 | { 306 | if (!$prefix) { 307 | $this->fallbackDirsPsr4 = (array) $paths; 308 | } else { 309 | $length = strlen($prefix); 310 | if ('\\' !== $prefix[$length - 1]) { 311 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 312 | } 313 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 314 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 315 | } 316 | } 317 | 318 | /** 319 | * Turns on searching the include path for class files. 320 | * 321 | * @param bool $useIncludePath 322 | * 323 | * @return void 324 | */ 325 | public function setUseIncludePath($useIncludePath) 326 | { 327 | $this->useIncludePath = $useIncludePath; 328 | } 329 | 330 | /** 331 | * Can be used to check if the autoloader uses the include path to check 332 | * for classes. 333 | * 334 | * @return bool 335 | */ 336 | public function getUseIncludePath() 337 | { 338 | return $this->useIncludePath; 339 | } 340 | 341 | /** 342 | * Turns off searching the prefix and fallback directories for classes 343 | * that have not been registered with the class map. 344 | * 345 | * @param bool $classMapAuthoritative 346 | * 347 | * @return void 348 | */ 349 | public function setClassMapAuthoritative($classMapAuthoritative) 350 | { 351 | $this->classMapAuthoritative = $classMapAuthoritative; 352 | } 353 | 354 | /** 355 | * Should class lookup fail if not found in the current class map? 356 | * 357 | * @return bool 358 | */ 359 | public function isClassMapAuthoritative() 360 | { 361 | return $this->classMapAuthoritative; 362 | } 363 | 364 | /** 365 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 366 | * 367 | * @param string|null $apcuPrefix 368 | * 369 | * @return void 370 | */ 371 | public function setApcuPrefix($apcuPrefix) 372 | { 373 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 374 | } 375 | 376 | /** 377 | * The APCu prefix in use, or null if APCu caching is not enabled. 378 | * 379 | * @return string|null 380 | */ 381 | public function getApcuPrefix() 382 | { 383 | return $this->apcuPrefix; 384 | } 385 | 386 | /** 387 | * Registers this instance as an autoloader. 388 | * 389 | * @param bool $prepend Whether to prepend the autoloader or not 390 | * 391 | * @return void 392 | */ 393 | public function register($prepend = false) 394 | { 395 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 396 | 397 | if (null === $this->vendorDir) { 398 | return; 399 | } 400 | 401 | if ($prepend) { 402 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 403 | } else { 404 | unset(self::$registeredLoaders[$this->vendorDir]); 405 | self::$registeredLoaders[$this->vendorDir] = $this; 406 | } 407 | } 408 | 409 | /** 410 | * Unregisters this instance as an autoloader. 411 | * 412 | * @return void 413 | */ 414 | public function unregister() 415 | { 416 | spl_autoload_unregister(array($this, 'loadClass')); 417 | 418 | if (null !== $this->vendorDir) { 419 | unset(self::$registeredLoaders[$this->vendorDir]); 420 | } 421 | } 422 | 423 | /** 424 | * Loads the given class or interface. 425 | * 426 | * @param string $class The name of the class 427 | * @return true|null True if loaded, null otherwise 428 | */ 429 | public function loadClass($class) 430 | { 431 | if ($file = $this->findFile($class)) { 432 | $includeFile = self::$includeFile; 433 | $includeFile($file); 434 | 435 | return true; 436 | } 437 | 438 | return null; 439 | } 440 | 441 | /** 442 | * Finds the path to the file where the class is defined. 443 | * 444 | * @param string $class The name of the class 445 | * 446 | * @return string|false The path if found, false otherwise 447 | */ 448 | public function findFile($class) 449 | { 450 | // class map lookup 451 | if (isset($this->classMap[$class])) { 452 | return $this->classMap[$class]; 453 | } 454 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 455 | return false; 456 | } 457 | if (null !== $this->apcuPrefix) { 458 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 459 | if ($hit) { 460 | return $file; 461 | } 462 | } 463 | 464 | $file = $this->findFileWithExtension($class, '.php'); 465 | 466 | // Search for Hack files if we are running on HHVM 467 | if (false === $file && defined('HHVM_VERSION')) { 468 | $file = $this->findFileWithExtension($class, '.hh'); 469 | } 470 | 471 | if (null !== $this->apcuPrefix) { 472 | apcu_add($this->apcuPrefix.$class, $file); 473 | } 474 | 475 | if (false === $file) { 476 | // Remember that this class does not exist. 477 | $this->missingClasses[$class] = true; 478 | } 479 | 480 | return $file; 481 | } 482 | 483 | /** 484 | * Returns the currently registered loaders indexed by their corresponding vendor directories. 485 | * 486 | * @return self[] 487 | */ 488 | public static function getRegisteredLoaders() 489 | { 490 | return self::$registeredLoaders; 491 | } 492 | 493 | /** 494 | * @param string $class 495 | * @param string $ext 496 | * @return string|false 497 | */ 498 | private function findFileWithExtension($class, $ext) 499 | { 500 | // PSR-4 lookup 501 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 502 | 503 | $first = $class[0]; 504 | if (isset($this->prefixLengthsPsr4[$first])) { 505 | $subPath = $class; 506 | while (false !== $lastPos = strrpos($subPath, '\\')) { 507 | $subPath = substr($subPath, 0, $lastPos); 508 | $search = $subPath . '\\'; 509 | if (isset($this->prefixDirsPsr4[$search])) { 510 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 511 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 512 | if (file_exists($file = $dir . $pathEnd)) { 513 | return $file; 514 | } 515 | } 516 | } 517 | } 518 | } 519 | 520 | // PSR-4 fallback dirs 521 | foreach ($this->fallbackDirsPsr4 as $dir) { 522 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 523 | return $file; 524 | } 525 | } 526 | 527 | // PSR-0 lookup 528 | if (false !== $pos = strrpos($class, '\\')) { 529 | // namespaced class name 530 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 531 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 532 | } else { 533 | // PEAR-like class name 534 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 535 | } 536 | 537 | if (isset($this->prefixesPsr0[$first])) { 538 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 539 | if (0 === strpos($class, $prefix)) { 540 | foreach ($dirs as $dir) { 541 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 542 | return $file; 543 | } 544 | } 545 | } 546 | } 547 | } 548 | 549 | // PSR-0 fallback dirs 550 | foreach ($this->fallbackDirsPsr0 as $dir) { 551 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 552 | return $file; 553 | } 554 | } 555 | 556 | // PSR-0 include paths. 557 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 558 | return $file; 559 | } 560 | 561 | return false; 562 | } 563 | 564 | /** 565 | * @return void 566 | */ 567 | private static function initializeIncludeClosure() 568 | { 569 | if (self::$includeFile !== null) { 570 | return; 571 | } 572 | 573 | /** 574 | * Scope isolated include. 575 | * 576 | * Prevents access to $this/self from included files. 577 | * 578 | * @param string $file 579 | * @return void 580 | */ 581 | self::$includeFile = \Closure::bind(static function($file) { 582 | include $file; 583 | }, null, null); 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /vendor/composer/InstalledVersions.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer; 14 | 15 | use Composer\Autoload\ClassLoader; 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * This class is copied in every Composer installed project and available to all 20 | * 21 | * See also https://getcomposer.org/doc/07-runtime.md#installed-versions 22 | * 23 | * To require its presence, you can require `composer-runtime-api ^2.0` 24 | * 25 | * @final 26 | */ 27 | class InstalledVersions 28 | { 29 | /** 30 | * @var mixed[]|null 31 | * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null 32 | */ 33 | private static $installed; 34 | 35 | /** 36 | * @var bool|null 37 | */ 38 | private static $canGetVendors; 39 | 40 | /** 41 | * @var array[] 42 | * @psalm-var array}> 43 | */ 44 | private static $installedByVendor = array(); 45 | 46 | /** 47 | * Returns a list of all package names which are present, either by being installed, replaced or provided 48 | * 49 | * @return string[] 50 | * @psalm-return list 51 | */ 52 | public static function getInstalledPackages() 53 | { 54 | $packages = array(); 55 | foreach (self::getInstalled() as $installed) { 56 | $packages[] = array_keys($installed['versions']); 57 | } 58 | 59 | if (1 === \count($packages)) { 60 | return $packages[0]; 61 | } 62 | 63 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); 64 | } 65 | 66 | /** 67 | * Returns a list of all package names with a specific type e.g. 'library' 68 | * 69 | * @param string $type 70 | * @return string[] 71 | * @psalm-return list 72 | */ 73 | public static function getInstalledPackagesByType($type) 74 | { 75 | $packagesByType = array(); 76 | 77 | foreach (self::getInstalled() as $installed) { 78 | foreach ($installed['versions'] as $name => $package) { 79 | if (isset($package['type']) && $package['type'] === $type) { 80 | $packagesByType[] = $name; 81 | } 82 | } 83 | } 84 | 85 | return $packagesByType; 86 | } 87 | 88 | /** 89 | * Checks whether the given package is installed 90 | * 91 | * This also returns true if the package name is provided or replaced by another package 92 | * 93 | * @param string $packageName 94 | * @param bool $includeDevRequirements 95 | * @return bool 96 | */ 97 | public static function isInstalled($packageName, $includeDevRequirements = true) 98 | { 99 | foreach (self::getInstalled() as $installed) { 100 | if (isset($installed['versions'][$packageName])) { 101 | return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; 102 | } 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /** 109 | * Checks whether the given package satisfies a version constraint 110 | * 111 | * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: 112 | * 113 | * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') 114 | * 115 | * @param VersionParser $parser Install composer/semver to have access to this class and functionality 116 | * @param string $packageName 117 | * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package 118 | * @return bool 119 | */ 120 | public static function satisfies(VersionParser $parser, $packageName, $constraint) 121 | { 122 | $constraint = $parser->parseConstraints((string) $constraint); 123 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); 124 | 125 | return $provided->matches($constraint); 126 | } 127 | 128 | /** 129 | * Returns a version constraint representing all the range(s) which are installed for a given package 130 | * 131 | * It is easier to use this via isInstalled() with the $constraint argument if you need to check 132 | * whether a given version of a package is installed, and not just whether it exists 133 | * 134 | * @param string $packageName 135 | * @return string Version constraint usable with composer/semver 136 | */ 137 | public static function getVersionRanges($packageName) 138 | { 139 | foreach (self::getInstalled() as $installed) { 140 | if (!isset($installed['versions'][$packageName])) { 141 | continue; 142 | } 143 | 144 | $ranges = array(); 145 | if (isset($installed['versions'][$packageName]['pretty_version'])) { 146 | $ranges[] = $installed['versions'][$packageName]['pretty_version']; 147 | } 148 | if (array_key_exists('aliases', $installed['versions'][$packageName])) { 149 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); 150 | } 151 | if (array_key_exists('replaced', $installed['versions'][$packageName])) { 152 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); 153 | } 154 | if (array_key_exists('provided', $installed['versions'][$packageName])) { 155 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); 156 | } 157 | 158 | return implode(' || ', $ranges); 159 | } 160 | 161 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 162 | } 163 | 164 | /** 165 | * @param string $packageName 166 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 167 | */ 168 | public static function getVersion($packageName) 169 | { 170 | foreach (self::getInstalled() as $installed) { 171 | if (!isset($installed['versions'][$packageName])) { 172 | continue; 173 | } 174 | 175 | if (!isset($installed['versions'][$packageName]['version'])) { 176 | return null; 177 | } 178 | 179 | return $installed['versions'][$packageName]['version']; 180 | } 181 | 182 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 183 | } 184 | 185 | /** 186 | * @param string $packageName 187 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 188 | */ 189 | public static function getPrettyVersion($packageName) 190 | { 191 | foreach (self::getInstalled() as $installed) { 192 | if (!isset($installed['versions'][$packageName])) { 193 | continue; 194 | } 195 | 196 | if (!isset($installed['versions'][$packageName]['pretty_version'])) { 197 | return null; 198 | } 199 | 200 | return $installed['versions'][$packageName]['pretty_version']; 201 | } 202 | 203 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 204 | } 205 | 206 | /** 207 | * @param string $packageName 208 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference 209 | */ 210 | public static function getReference($packageName) 211 | { 212 | foreach (self::getInstalled() as $installed) { 213 | if (!isset($installed['versions'][$packageName])) { 214 | continue; 215 | } 216 | 217 | if (!isset($installed['versions'][$packageName]['reference'])) { 218 | return null; 219 | } 220 | 221 | return $installed['versions'][$packageName]['reference']; 222 | } 223 | 224 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 225 | } 226 | 227 | /** 228 | * @param string $packageName 229 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. 230 | */ 231 | public static function getInstallPath($packageName) 232 | { 233 | foreach (self::getInstalled() as $installed) { 234 | if (!isset($installed['versions'][$packageName])) { 235 | continue; 236 | } 237 | 238 | return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; 239 | } 240 | 241 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 242 | } 243 | 244 | /** 245 | * @return array 246 | * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} 247 | */ 248 | public static function getRootPackage() 249 | { 250 | $installed = self::getInstalled(); 251 | 252 | return $installed[0]['root']; 253 | } 254 | 255 | /** 256 | * Returns the raw installed.php data for custom implementations 257 | * 258 | * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. 259 | * @return array[] 260 | * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} 261 | */ 262 | public static function getRawData() 263 | { 264 | @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); 265 | 266 | if (null === self::$installed) { 267 | // only require the installed.php file if this file is loaded from its dumped location, 268 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 269 | if (substr(__DIR__, -8, 1) !== 'C') { 270 | self::$installed = include __DIR__ . '/installed.php'; 271 | } else { 272 | self::$installed = array(); 273 | } 274 | } 275 | 276 | return self::$installed; 277 | } 278 | 279 | /** 280 | * Returns the raw data of all installed.php which are currently loaded for custom implementations 281 | * 282 | * @return array[] 283 | * @psalm-return list}> 284 | */ 285 | public static function getAllRawData() 286 | { 287 | return self::getInstalled(); 288 | } 289 | 290 | /** 291 | * Lets you reload the static array from another file 292 | * 293 | * This is only useful for complex integrations in which a project needs to use 294 | * this class but then also needs to execute another project's autoloader in process, 295 | * and wants to ensure both projects have access to their version of installed.php. 296 | * 297 | * A typical case would be PHPUnit, where it would need to make sure it reads all 298 | * the data it needs from this class, then call reload() with 299 | * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure 300 | * the project in which it runs can then also use this class safely, without 301 | * interference between PHPUnit's dependencies and the project's dependencies. 302 | * 303 | * @param array[] $data A vendor/composer/installed.php data set 304 | * @return void 305 | * 306 | * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data 307 | */ 308 | public static function reload($data) 309 | { 310 | self::$installed = $data; 311 | self::$installedByVendor = array(); 312 | } 313 | 314 | /** 315 | * @return array[] 316 | * @psalm-return list}> 317 | */ 318 | private static function getInstalled() 319 | { 320 | if (null === self::$canGetVendors) { 321 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); 322 | } 323 | 324 | $installed = array(); 325 | 326 | if (self::$canGetVendors) { 327 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 328 | if (isset(self::$installedByVendor[$vendorDir])) { 329 | $installed[] = self::$installedByVendor[$vendorDir]; 330 | } elseif (is_file($vendorDir.'/composer/installed.php')) { 331 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 332 | $required = require $vendorDir.'/composer/installed.php'; 333 | $installed[] = self::$installedByVendor[$vendorDir] = $required; 334 | if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 335 | self::$installed = $installed[count($installed) - 1]; 336 | } 337 | } 338 | } 339 | } 340 | 341 | if (null === self::$installed) { 342 | // only require the installed.php file if this file is loaded from its dumped location, 343 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 344 | if (substr(__DIR__, -8, 1) !== 'C') { 345 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 346 | $required = require __DIR__ . '/installed.php'; 347 | self::$installed = $required; 348 | } else { 349 | self::$installed = array(); 350 | } 351 | } 352 | 353 | if (self::$installed !== array()) { 354 | $installed[] = self::$installed; 355 | } 356 | 357 | return $installed; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /vendor/composer/autoload_classmap.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | 'Kirby\\ComposerInstaller\\CmsInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', 11 | 'Kirby\\ComposerInstaller\\Installer' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', 12 | 'Kirby\\ComposerInstaller\\Plugin' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', 13 | 'Kirby\\ComposerInstaller\\PluginInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', 14 | 'Thumbhash\\Thumbhash' => $vendorDir . '/srwiez/thumbhash/src/Thumbhash.php', 15 | 'tobimori\\ThumbHash' => $baseDir . '/classes/ThumbHash.php', 16 | ); 17 | -------------------------------------------------------------------------------- /vendor/composer/autoload_files.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/srwiez/thumbhash/src/helpers.php', 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/classes'), 10 | 'Thumbhash\\' => array($vendorDir . '/srwiez/thumbhash/src'), 11 | 'Kirby\\' => array($vendorDir . '/getkirby/composer-installer/src'), 12 | ); 13 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | $filesToLoad = \Composer\Autoload\ComposerStaticInitdffd8c9e0a563d9c7e328275c53882db::$files; 37 | $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { 38 | if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { 39 | $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; 40 | 41 | require $file; 42 | } 43 | }, null, null); 44 | foreach ($filesToLoad as $fileIdentifier => $file) { 45 | $requireFile($fileIdentifier, $file); 46 | } 47 | 48 | return $loader; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/..' . '/srwiez/thumbhash/src/helpers.php', 11 | ); 12 | 13 | public static $prefixLengthsPsr4 = array ( 14 | 't' => 15 | array ( 16 | 'tobimori\\' => 9, 17 | ), 18 | 'T' => 19 | array ( 20 | 'Thumbhash\\' => 10, 21 | ), 22 | 'K' => 23 | array ( 24 | 'Kirby\\' => 6, 25 | ), 26 | ); 27 | 28 | public static $prefixDirsPsr4 = array ( 29 | 'tobimori\\' => 30 | array ( 31 | 0 => __DIR__ . '/../..' . '/classes', 32 | ), 33 | 'Thumbhash\\' => 34 | array ( 35 | 0 => __DIR__ . '/..' . '/srwiez/thumbhash/src', 36 | ), 37 | 'Kirby\\' => 38 | array ( 39 | 0 => __DIR__ . '/..' . '/getkirby/composer-installer/src', 40 | ), 41 | ); 42 | 43 | public static $classMap = array ( 44 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 45 | 'Kirby\\ComposerInstaller\\CmsInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', 46 | 'Kirby\\ComposerInstaller\\Installer' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', 47 | 'Kirby\\ComposerInstaller\\Plugin' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', 48 | 'Kirby\\ComposerInstaller\\PluginInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', 49 | 'Thumbhash\\Thumbhash' => __DIR__ . '/..' . '/srwiez/thumbhash/src/Thumbhash.php', 50 | 'tobimori\\ThumbHash' => __DIR__ . '/../..' . '/classes/ThumbHash.php', 51 | ); 52 | 53 | public static function getInitializer(ClassLoader $loader) 54 | { 55 | return \Closure::bind(function () use ($loader) { 56 | $loader->prefixLengthsPsr4 = ComposerStaticInitdffd8c9e0a563d9c7e328275c53882db::$prefixLengthsPsr4; 57 | $loader->prefixDirsPsr4 = ComposerStaticInitdffd8c9e0a563d9c7e328275c53882db::$prefixDirsPsr4; 58 | $loader->classMap = ComposerStaticInitdffd8c9e0a563d9c7e328275c53882db::$classMap; 59 | 60 | }, null, ClassLoader::class); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'tobimori/kirby-blurhash', 4 | 'pretty_version' => '1.2.0', 5 | 'version' => '1.2.0.0', 6 | 'reference' => NULL, 7 | 'type' => 'kirby-plugin', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => false, 11 | ), 12 | 'versions' => array( 13 | 'getkirby/composer-installer' => array( 14 | 'pretty_version' => '1.2.1', 15 | 'version' => '1.2.1.0', 16 | 'reference' => 'c98ece30bfba45be7ce457e1102d1b169d922f3d', 17 | 'type' => 'composer-plugin', 18 | 'install_path' => __DIR__ . '/../getkirby/composer-installer', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'srwiez/thumbhash' => array( 23 | 'pretty_version' => '1.1.0', 24 | 'version' => '1.1.0.0', 25 | 'reference' => 'eae467fa06c6aeb96c6910dc1ccb71af21cc1dca', 26 | 'type' => 'library', 27 | 'install_path' => __DIR__ . '/../srwiez/thumbhash', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | 'tobimori/kirby-blurhash' => array( 32 | 'pretty_version' => '1.2.0', 33 | 'version' => '1.2.0.0', 34 | 'reference' => NULL, 35 | 'type' => 'kirby-plugin', 36 | 'install_path' => __DIR__ . '/../../', 37 | 'aliases' => array(), 38 | 'dev_requirement' => false, 39 | ), 40 | ), 41 | ); 42 | -------------------------------------------------------------------------------- /vendor/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 80000)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /vendor/srwiez/thumbhash/src/Thumbhash.php: -------------------------------------------------------------------------------- 1 | 100 || $h > 100) { 20 | throw new \Exception("{$w}x{$h} doesn't fit in 100x100"); 21 | } 22 | 23 | // Determine the average color 24 | $avg_r = 0; 25 | $avg_g = 0; 26 | $avg_b = 0; 27 | $avg_a = 0; 28 | 29 | // TODO : use array_chunk ? 30 | for ($i = 0, $j = 0; $i < $w * $h; $i++, $j += 4) { 31 | $alpha = $rgba[$j + 3] / 255; 32 | $avg_r += $alpha / 255 * $rgba[$j]; 33 | $avg_g += $alpha / 255 * $rgba[$j + 1]; 34 | $avg_b += $alpha / 255 * $rgba[$j + 2]; 35 | $avg_a += $alpha; 36 | } 37 | if ($avg_a) { 38 | $avg_r /= $avg_a; 39 | $avg_g /= $avg_a; 40 | $avg_b /= $avg_a; 41 | } 42 | 43 | $hasAlpha = $avg_a < $w * $h; 44 | $l_limit = $hasAlpha ? 5 : 7; // Use fewer luminance bits if there's alpha 45 | $lx = max(1, round($l_limit * $w / max($w, $h))); 46 | $ly = max(1, round($l_limit * $h / max($w, $h))); 47 | $l = []; // luminance 48 | $p = []; // yellow - blue 49 | $q = []; // red - green 50 | $a = []; // alpha 51 | 52 | // TODO : use array_chunk ? 53 | // Convert the image from RGBA to LPQA (composite atop the average color) 54 | for ($i = 0, $j = 0; $i < $w * $h; $i++, $j += 4) { 55 | $alpha = $rgba[$j + 3] / 255; 56 | $r = $avg_r * (1 - $alpha) + $alpha / 255 * $rgba[$j]; 57 | $g = $avg_g * (1 - $alpha) + $alpha / 255 * $rgba[$j + 1]; 58 | $b = $avg_b * (1 - $alpha) + $alpha / 255 * $rgba[$j + 2]; 59 | $l[$i] = ($r + $g + $b) / 3; 60 | $p[$i] = ($r + $g) / 2 - $b; 61 | $q[$i] = $r - $g; 62 | $a[$i] = $alpha; 63 | } 64 | 65 | // var_dump($l, $p, $q, $a); 66 | // Encode using the DCT into DC (constant) and normalized AC (varying) terms 67 | list($l_dc, $l_ac, $l_scale) = static::encodeChannel($l, max(3, $lx), max(3, $ly), $w, $h); 68 | list($p_dc, $p_ac, $p_scale) = static::encodeChannel($p, 3, 3, $w, $h); 69 | list($q_dc, $q_ac, $q_scale) = static::encodeChannel($q, 3, 3, $w, $h); 70 | list($a_dc, $a_ac, $a_scale) = $hasAlpha ? static::encodeChannel($a, 5, 5, $w, $h) : [0,0,0]; 71 | 72 | $isLandscape = $w > $h; 73 | $header24 = round(63 * $l_dc) | (round(31.5 + 31.5 * $p_dc) << 6) | (round(31.5 + 31.5 * $q_dc) << 12) | (round(31 * $l_scale) << 18) | ($hasAlpha << 23); 74 | $header16 = ($isLandscape ? $ly : $lx) | (round(63 * $p_scale) << 3) | (round(63 * $q_scale) << 9) | ($isLandscape << 15); 75 | $hash = [$header24 & 255, ($header24 >> 8) & 255, $header24 >> 16, $header16 & 255, $header16 >> 8]; 76 | $ac_start = $hasAlpha ? 6 : 5; 77 | $ac_index = 0; 78 | 79 | if ($hasAlpha) { 80 | $hash[] = round(15 * $a_dc) | (round(15 * $a_scale) << 4); 81 | } 82 | 83 | $acList = $hasAlpha ? [$l_ac, $p_ac, $q_ac, $a_ac] : [$l_ac, $p_ac, $q_ac]; 84 | 85 | foreach ($acList as $ac) { 86 | foreach ($ac as $f) { 87 | $hash_key = $ac_start + ($ac_index >> 1); 88 | if (!isset($hash[$hash_key])) { 89 | $hash[$hash_key] = 0; 90 | } 91 | $hash[$hash_key] |= round(15 * $f) << (($ac_index++ & 1) << 2); 92 | } 93 | } 94 | 95 | return $hash; 96 | } 97 | 98 | /* 99 | * Encode a channel using the Discrete Cosine Transform (DCT) 100 | * into DC (constant) and normalized AC (varying) terms. 101 | */ 102 | public static function encodeChannel($channel, $nx, $ny, $w, $h) 103 | { 104 | $dc = 0; 105 | $ac = []; 106 | $scale = 0; 107 | $fx = []; 108 | 109 | for ($cy = 0; $cy < $ny; $cy++) { 110 | for ($cx = 0; $cx * $ny < $nx * ($ny - $cy); $cx++) { 111 | $f = 0; 112 | for ($x = 0; $x < $w; $x++) { 113 | $fx[$x] = cos(pi() / $w * $cx * ($x + 0.5)); 114 | } 115 | for ($y = 0; $y < $h; $y++) { 116 | for ($x = 0, $fy = cos(pi() / $h * $cy * ($y + 0.5)); $x < $w; $x++) { 117 | $f += $channel[$x + $y * $w] * $fx[$x] * $fy; 118 | } 119 | } 120 | $f /= $w * $h; 121 | if ($cx || $cy) { 122 | $ac[] = $f; 123 | $scale = max($scale, abs($f)); 124 | } else { 125 | $dc = $f; 126 | } 127 | } 128 | } 129 | if ($scale) { 130 | for ($i = 0; $i < count($ac); $i++) { 131 | $ac[$i] = 0.5 + 0.5 / $scale * $ac[$i]; 132 | } 133 | } 134 | 135 | return [$dc, $ac, $scale]; 136 | } 137 | 138 | public static function convertHashToString(array $hash): string 139 | { 140 | return rtrim(base64_encode(implode(array_map("chr", $hash))), '='); 141 | } 142 | 143 | public static function convertStringToHash(string $str): array 144 | { 145 | return array_map("ord", str_split(base64_decode($str . "="))); 146 | } 147 | 148 | /** 149 | * Decodes a ThumbHash to an RGBA image. RGB is not premultiplied by A. 150 | * 151 | * @param array $hash The bytes of the ThumbHash. 152 | * @return array The width, height, and pixels of the rendered placeholder image. 153 | */ 154 | public static function hashToRGBA(array $hash): array 155 | { 156 | // Read the constants 157 | $header24 = $hash[0] | ($hash[1] << 8) | ($hash[2] << 16); 158 | $header16 = $hash[3] | ($hash[4] << 8); 159 | $l_dc = ($header24 & 63) / 63; 160 | $p_dc = (($header24 >> 6) & 63) / 31.5 - 1; 161 | $q_dc = (($header24 >> 12) & 63) / 31.5 - 1; 162 | $l_scale = (($header24 >> 18) & 31) / 31; 163 | $hasAlpha = $header24 >> 23; 164 | $p_scale = (($header16 >> 3) & 63) / 63; 165 | $q_scale = (($header16 >> 9) & 63) / 63; 166 | $isLandscape = $header16 >> 15; 167 | $lx = max(3, ($isLandscape ? $hasAlpha ? 5 : 7 : $header16 & 7)); 168 | $ly = max(3, ($isLandscape ? $header16 & 7 : ($hasAlpha ? 5 : 7))); 169 | $a_dc = $hasAlpha ? ($hash[5] & 15) / 15 : 1; 170 | $a_scale = ($hash[5] >> 4) / 15; 171 | 172 | // Read the varying factors (boost saturation by 1.25x to compensate for quantization) 173 | $ac_start = $hasAlpha ? 6 : 5; 174 | $ac_index = 0; 175 | $decodeChannel = function ($nx, $ny, $scale) use (&$hash, &$ac_start, &$ac_index) { 176 | $ac = []; 177 | for ($cy = 0; $cy < $ny; $cy++) { 178 | for ($cx = $cy ? 0 : 1; $cx * $ny < $nx * ($ny - $cy); $cx++) { 179 | $ac[] = ((((int)$hash[$ac_start + ($ac_index >> 1)] >> (($ac_index++ & 1) << 2)) & 15) / 7.5 - 1) * $scale; 180 | } 181 | } 182 | 183 | return $ac; 184 | }; 185 | $l_ac = $decodeChannel($lx, $ly, $l_scale); 186 | $p_ac = $decodeChannel(3, 3, $p_scale * 1.25); 187 | $q_ac = $decodeChannel(3, 3, $q_scale * 1.25); 188 | $a_ac = $hasAlpha ? $decodeChannel(5, 5, $a_scale) : null; 189 | // Decode using the DCT into RGB 190 | $ratio = static::toApproximateAspectRatio($hash); 191 | $w = round($ratio > 1 ? 32 : 32 * $ratio); 192 | $h = round($ratio > 1 ? 32 / $ratio : 32); 193 | $rgba = []; 194 | $fx = []; 195 | $fy = []; 196 | 197 | for ($y = 0, $i = 0; $y < $h; $y++) { 198 | for ($x = 0; $x < $w; $x++, $i += 4) { 199 | $l = $l_dc; 200 | $p = $p_dc; 201 | $q = $q_dc; 202 | $a = $a_dc; 203 | 204 | // Precompute the coefficients 205 | for ($cx = 0, $n = max($lx, $hasAlpha ? 5 : 3); $cx < $n; $cx++) { 206 | $fx[$cx] = cos(M_PI / $w * ($x + 0.5) * $cx); 207 | } 208 | for ($cy = 0, $n = max($ly, $hasAlpha ? 5 : 3); $cy < $n; $cy++) { 209 | $fy[$cy] = cos(M_PI / $h * ($y + 0.5) * $cy); 210 | } 211 | 212 | // Decode L 213 | for ($cy = 0, $j = 0; $cy < $ly; $cy++) { 214 | for ($cx = $cy ? 0 : 1, $fy2 = $fy[$cy] * 2; $cx * $ly < $lx * ($ly - $cy); $cx++, $j++) { 215 | $l += $l_ac[$j] * $fx[$cx] * $fy2; 216 | } 217 | } 218 | 219 | // Decode P and Q 220 | for ($cy = 0, $j = 0; $cy < 3; $cy++) { 221 | for ($cx = $cy ? 0 : 1, $fy2 = $fy[$cy] * 2; $cx < 3 - $cy; $cx++, $j++) { 222 | $f = $fx[$cx] * $fy2; 223 | $p += $p_ac[$j] * $f; 224 | $q += $q_ac[$j] * $f; 225 | } 226 | } 227 | 228 | // Decode A 229 | if ($hasAlpha) { 230 | for ($cy = 0, $j = 0; $cy < 5; $cy++) { 231 | for ($cx = $cy ? 0 : 1, $fy2 = $fy[$cy] * 2; $cx < 5 - $cy; $cx++, $j++) { 232 | $a += $a_ac[$j] * $fx[$cx] * $fy2; 233 | } 234 | } 235 | } 236 | 237 | // Convert to RGB 238 | $b = $l - 2 / 3 * $p; 239 | $r = (3 * $l - $b + $q) / 2; 240 | $g = $r - $q; 241 | $rgba[$i] = max(0, 255 * min(1, $r)); 242 | $rgba[$i + 1] = max(0, 255 * min(1, $g)); 243 | $rgba[$i + 2] = max(0, 255 * min(1, $b)); 244 | $rgba[$i + 3] = max(0, 255 * min(1, $a)); 245 | } 246 | } 247 | 248 | return compact('w', 'h', 'rgba'); 249 | } 250 | 251 | 252 | /** 253 | * Extracts the average color from a ThumbHash. RGB is not premultiplied by A. 254 | * 255 | * @param array $hash The bytes of the ThumbHash. 256 | * @return array The RGBA values for the average color. Each value ranges from 0 to 1. 257 | */ 258 | function toAverageRGBA(array $hash): array 259 | { 260 | $header = $hash[0] | ($hash[1] << 8) | ($hash[2] << 16); 261 | $l = ($header & 63) / 63; 262 | $p = (($header >> 6) & 63) / 31.5 - 1; 263 | $q = (($header >> 12) & 63) / 31.5 - 1; 264 | $hasAlpha = $header >> 23; 265 | $a = $hasAlpha ? ($hash[5] & 15) / 15 : 1; 266 | $b = $l - 2 / 3 * $p; 267 | $r = (3 * $l - $b + $q) / 2; 268 | $g = $r - $q; 269 | 270 | return [ 271 | 'r' => max(0, min(1, $r)), 272 | 'g' => max(0, min(1, $g)), 273 | 'b' => max(0, min(1, $b)), 274 | 'a' => $a 275 | ]; 276 | } 277 | 278 | 279 | /** 280 | * Extracts the approximate aspect ratio of the original image. 281 | * 282 | * @param array $hash The bytes of the ThumbHash. 283 | * @return float The approximate aspect ratio (i.e. width / height). 284 | */ 285 | public static function toApproximateAspectRatio(array $hash) 286 | { 287 | $header = $hash[3]; 288 | $hasAlpha = $hash[2] & 0x80; 289 | $isLandscape = $hash[4] & 0x80; 290 | $lx = $isLandscape ? ($hasAlpha ? 5 : 7) : ($header & 7); 291 | $ly = $isLandscape ? ($header & 7) : ($hasAlpha ? 5 : 7); 292 | 293 | return $lx / $ly; 294 | } 295 | 296 | /** 297 | * Encodes an RGBA image to a PNG data URL. RGB should not be premultiplied by 298 | * A. This is optimized for speed and simplicity and does not optimize for size 299 | * at all. This doesn't do any compression (all values are stored uncompressed). 300 | * 301 | * @param $w Int The width of the input image. Must be ≤100px. 302 | * @param $h Int The height of the input image. Must be ≤100px. 303 | * @param $rgba String The pixels in the input image, row-by-row. Must have w*h*4 elements. 304 | * @returns String A data URL containing a PNG for the input image. 305 | */ 306 | public static function rgbaToDataURL(int $w, int $h, $rgba) 307 | { 308 | $row = $w * 4 + 1; 309 | $idat = 6 + $h * (5 + $row); 310 | $bytes = [ 311 | 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 312 | $w >> 8, $w & 255, 0, 0, $h >> 8, $h & 255, 8, 6, 0, 0, 0, 0, 0, 0, 0, 313 | ($idat >> 24) & 0xFF, ($idat >> 16) & 255, ($idat >> 8) & 255, $idat & 255, 314 | 73, 68, 65, 84, 120, 1 315 | ]; 316 | $table = [ 317 | 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960, 318 | 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376, 319 | -2032938284, -1609899400, -1111625188 320 | ]; 321 | $a = 1; 322 | $b = 0; 323 | $y = 0; 324 | $i = 0; 325 | $end = $row - 1; 326 | 327 | for (; $y < $h; $y++, $end += $row - 1) { 328 | array_push($bytes, ($y + 1 < $h ? 0 : 1), $row & 255, $row >> 8, ~$row & 255, ($row >> 8) ^ 255, 0); 329 | 330 | for ($b = ($b + $a) % 65521; $i < $end; $i++) { 331 | $u = intval($rgba[$i]) & 255; 332 | $bytes[] = $u; 333 | $a = ($a + $u) % 65521; 334 | $b = ($b + $a) % 65521; 335 | } 336 | } 337 | 338 | array_push($bytes, 339 | $b >> 8, $b & 255, $a >> 8, $a & 255, 0, 0, 0, 0, 340 | 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 341 | ); 342 | 343 | $ranges = [[12, 29], [37, 41 + $idat]]; 344 | 345 | foreach ($ranges as $range) { 346 | $start = $range[0]; 347 | $end = $range[1]; 348 | $c = ~0; 349 | 350 | for ($i = $start; $i < $end; $i++) { 351 | $c ^= $bytes[$i]; 352 | $c = (($c >> 4) & 0xFFFFFFF) ^ $table[$c & 15]; 353 | $c = (($c >> 4) & 0xFFFFFFF) ^ $table[$c & 15]; 354 | } 355 | 356 | $c = ~$c; 357 | $bytes[$end++] = ($c >> 24) & 0xFF; 358 | $bytes[$end++] = ($c >> 16) & 255; 359 | $bytes[$end++] = ($c >> 8) & 255; 360 | $bytes[$end++] = $c & 255; 361 | } 362 | 363 | 364 | return 'data:image/png;base64,' . base64_encode(implode('', array_map('chr', $bytes))); 365 | } 366 | 367 | 368 | /** 369 | * Decodes a ThumbHash to a PNG data URL. This is a convenience function that 370 | * just calls "thumbHashToRGBA" followed by "rgbaToDataURL". 371 | * 372 | * @param array $hash The bytes of the ThumbHash. 373 | * @returns array A data URL containing a PNG for the rendered ThumbHash. 374 | */ 375 | public static function toDataURL(array $hash): string 376 | { 377 | $image = static::hashToRGBA($hash); 378 | 379 | return static::rgbaToDataURL($image['w'], $image['h'], $image['rgba']); 380 | } 381 | 382 | } -------------------------------------------------------------------------------- /vendor/srwiez/thumbhash/src/helpers.php: -------------------------------------------------------------------------------- 1 | readImageBlob($content); 41 | 42 | $width = $image->getImageWidth(); 43 | $height = $image->getImageHeight(); 44 | 45 | $pixels = []; 46 | for ($y = 0; $y < $height; $y++) { 47 | for ($x = 0; $x < $width; $x++) { 48 | 49 | $pixel = $image->getImagePixelColor($x, $y); 50 | $colors = $pixel->getColor(2); 51 | $pixels[] = $colors['r']; 52 | $pixels[] = $colors['g']; 53 | $pixels[] = $colors['b']; 54 | $pixels[] = $colors['a']; 55 | } 56 | } 57 | return [$width, $height, $pixels]; 58 | } 59 | 60 | --------------------------------------------------------------------------------