├── 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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------