├── .coveralls.yml ├── .github └── workflows │ ├── static-analysis.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── infection.json.dist ├── phpstan.neon.dist ├── phpunit.xml.dist ├── psalm.xml ├── src ├── AC.php ├── Base83.php ├── Blurhash.php ├── Color.php └── DC.php └── test ├── ACTest.php ├── Base83Test.php ├── BlurhashTest.php ├── DCTest.php └── data ├── L6Pj.json ├── LEHV.json ├── LGFO.json ├── LKO2.json ├── img1.jpg ├── img2.jpg ├── img3.jpg └── img4.jpg /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml 3 | json_path: coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis (informative) 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | phpstan: 10 | name: PHPStan 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: 8.2 17 | coverage: none 18 | 19 | - run: composer install --no-progress --prefer-dist 20 | - run: composer require phpstan/phpstan --no-progress --dev 21 | - run: vendor/bin/phpstan analyse src/ 22 | continue-on-error: true # is only informative 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | linux: 8 | name: Test on Linux 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php-version: ['8.1', '8.2', '8.3'] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 1 19 | 20 | - run: php${{ matrix.php-version }} -v 21 | - run: php${{ matrix.php-version }} -m 22 | - run: composer -V 23 | - run: composer install --no-progress 24 | - run: php${{ matrix.php-version }} vendor/bin/phpunit 25 | 26 | windows: 27 | name: Test on Windows 28 | defaults: 29 | run: 30 | shell: cmd 31 | runs-on: windows-latest 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | php-version: ['8.1', '8.2', '8.3'] 36 | arch: [x64] 37 | ts: [nts] 38 | 39 | steps: 40 | - name: Setup PHP 41 | id: setup-php 42 | uses: php/setup-php-sdk@v0.10 43 | with: 44 | version: ${{matrix.php-version}} 45 | arch: ${{matrix.arch}} 46 | ts: ${{matrix.ts}} 47 | - uses: actions/checkout@v2 48 | with: 49 | fetch-depth: 1 50 | 51 | - run: php -v 52 | - run: echo extension=gd>>C:\tools\php\php.ini 53 | - run: php -m 54 | - run: composer -V 55 | - run: composer install --no-progress 56 | - run: php vendor/bin/phpunit 57 | 58 | code_coverage: 59 | name: Code Coverage 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v2 63 | - uses: shivammathur/setup-php@v2 64 | with: 65 | php-version: 8.3 66 | coverage: none 67 | 68 | - run: composer install --no-progress 69 | - run: mkdir -p build/logs 70 | - run: phpdbg -qrr vendor/bin/phpunit 71 | - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.7.0/php-coveralls.phar 72 | - env: 73 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: php php-coveralls.phar --verbose 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | composer.lock 4 | 5 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 6 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 7 | # composer.lock 8 | build/ 9 | .phpunit.result.cache 10 | infection.log 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Boris Momčilović 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 | # php-blurhash [![Tests](https://github.com/kornrunner/php-blurhash/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/kornrunner/php-blurhash/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/github/kornrunner/php-blurhash/badge.svg?branch=master)](https://coveralls.io/github/kornrunner/php-blurhash?branch=master) [![Latest Stable Version](https://poser.pugx.org/kornrunner/blurhash/v/stable)](https://packagist.org/packages/kornrunner/blurhash) 2 | 3 | 4 | A pure PHP implementation of [Blurhash](https://github.com/woltapp/blurhash). The API is stable, however the hashing function in either direction may not be. 5 | 6 | Blurhash is an algorithm written by [Dag Ågren](https://github.com/DagAgren) for [Wolt (woltapp/blurhash)](https://github.com/woltapp/blurhash) that encodes an image into a short (~20-30 byte) ASCII string. When you decode the string back into an image, you get a gradient of colors that represent the original image. This can be useful for scenarios where you want an image placeholder before loading, or even to censor the contents of an image [a la Mastodon](https://blog.joinmastodon.org/2019/05/improving-support-for-adult-content-on-mastodon/). 7 | 8 | ## Installation 9 | 10 | 11 | ```sh 12 | $ composer require kornrunner/blurhash 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Encoding with GD 18 | Encoding an image to blurhash expects two-dimensional array of colors of image pixels, sample code: 19 | 20 | ```php 21 | read($file); 67 | $width = $image->width(); 68 | $height = $image->height(); 69 | 70 | $pixels = []; 71 | for ($y = 0; $y < $height; ++$y) { 72 | $row = []; 73 | for ($x = 0; $x < $width; ++$x) { 74 | $colors = $image->pickColor($x, $y); 75 | 76 | if (!($colors instanceof RgbColor)) { 77 | $colors = $colors->convertTo(new RgbColorspace()); 78 | } 79 | 80 | $row[] = [ 81 | $colors->channel(Red::class)->value(), 82 | $colors->channel(Green::class)->value(), 83 | $colors->channel(Blue::class)->value(), 84 | ]; 85 | } 86 | $pixels[] = $row; 87 | } 88 | 89 | $components_x = 4; 90 | $components_y = 3; 91 | $blurhash = Blurhash::encode($pixels, $components_x, $components_y); 92 | // LEHV9uae2yk8pyo0adR*.7kCMdnj 93 | ``` 94 | 95 | ### Decoding with JS / TS 96 | 97 | For decoding of blurhash people will likely go for some other implementation ([JavaScript/TypeScript](https://github.com/woltapp/blurhash/tree/master/TypeScript)). 98 | PHP decoder returns a pixel array that can be used to generate the image: 99 | 100 | ```php 101 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | test 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/AC.php: -------------------------------------------------------------------------------- 1 | 0; 32 | return $sign * pow(abs($base), $exp); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Base83.php: -------------------------------------------------------------------------------- 1 | 9) || ($components_y < 1 || $components_y > 9)) { 11 | throw new InvalidArgumentException("x and y component counts must be between 1 and 9 inclusive."); 12 | } 13 | $height = count($image); 14 | $width = count($image[0]); 15 | 16 | $image_linear = $image; 17 | if (!$linear) { 18 | $image_linear = []; 19 | for ($y = 0; $y < $height; $y++) { 20 | $line = []; 21 | for ($x = 0; $x < $width; $x++) { 22 | $pixel = $image[$y][$x]; 23 | $line[] = [ 24 | Color::toLinear($pixel[0]), 25 | Color::toLinear($pixel[1]), 26 | Color::toLinear($pixel[2]) 27 | ]; 28 | } 29 | $image_linear[] = $line; 30 | } 31 | } 32 | 33 | $components = []; 34 | $scale = 1 / ($width * $height); 35 | for ($y = 0; $y < $components_y; $y++) { 36 | for ($x = 0; $x < $components_x; $x++) { 37 | $normalisation = $x == 0 && $y == 0 ? 1 : 2; 38 | $r = $g = $b = 0; 39 | for ($i = 0; $i < $width; $i++) { 40 | for ($j = 0; $j < $height; $j++) { 41 | $color = $image_linear[$j][$i]; 42 | $basis = $normalisation 43 | * cos(M_PI * $i * $x / $width) 44 | * cos(M_PI * $j * $y / $height); 45 | 46 | $r += $basis * $color[0]; 47 | $g += $basis * $color[1]; 48 | $b += $basis * $color[2]; 49 | } 50 | } 51 | 52 | $components[] = [ 53 | $r * $scale, 54 | $g * $scale, 55 | $b * $scale 56 | ]; 57 | } 58 | } 59 | 60 | $dc_value = DC::encode(array_shift($components) ?: []); 61 | 62 | $max_ac_component = 0; 63 | foreach ($components as $component) { 64 | $component[] = $max_ac_component; 65 | $max_ac_component = max ($component); 66 | } 67 | 68 | $quant_max_ac_component = (int) max(0, min(82, floor($max_ac_component * 166 - 0.5))); 69 | $ac_component_norm_factor = ($quant_max_ac_component + 1) / 166; 70 | 71 | $ac_values = []; 72 | foreach ($components as $component) { 73 | $ac_values[] = AC::encode($component, $ac_component_norm_factor); 74 | } 75 | 76 | $blurhash = Base83::encode($components_x - 1 + ($components_y - 1) * 9, 1); 77 | $blurhash .= Base83::encode($quant_max_ac_component, 1); 78 | $blurhash .= Base83::encode($dc_value, 4); 79 | foreach ($ac_values as $ac_value) { 80 | $blurhash .= Base83::encode((int) $ac_value, 2); 81 | } 82 | 83 | return $blurhash; 84 | } 85 | 86 | public static function decode (string $blurhash, int $width, int $height, float $punch = 1.0, bool $linear = false): array { 87 | if (empty($blurhash) || strlen($blurhash) < 6) { 88 | throw new InvalidArgumentException("Blurhash string must be at least 6 characters"); 89 | } 90 | 91 | $size_info = Base83::decode($blurhash[0]); 92 | $size_y = intdiv($size_info, 9) + 1; 93 | $size_x = ($size_info % 9) + 1; 94 | 95 | $length = strlen($blurhash); 96 | $expected_length = (int) (4 + (2 * $size_y * $size_x)); 97 | if ($length !== $expected_length) { 98 | throw new InvalidArgumentException("Blurhash length mismatch: length is {$length} but it should be {$expected_length}"); 99 | } 100 | 101 | $colors = [DC::decode(Base83::decode(substr($blurhash, 2, 4)))]; 102 | 103 | $quant_max_ac_component = Base83::decode($blurhash[1]); 104 | $max_value = ($quant_max_ac_component + 1) / 166; 105 | for ($i = 1; $i < $size_x * $size_y; $i++) { 106 | $value = Base83::decode(substr($blurhash, 4 + $i * 2, 2)); 107 | $colors[$i] = AC::decode($value, $max_value * $punch); 108 | } 109 | 110 | $pixels = []; 111 | for ($y = 0; $y < $height; $y++) { 112 | $row = []; 113 | for ($x = 0; $x < $width; $x++) { 114 | $r = $g = $b = 0; 115 | for ($j = 0; $j < $size_y; $j++) { 116 | for ($i = 0; $i < $size_x; $i++) { 117 | $color = $colors[$i + $j * $size_x]; 118 | $basis = 119 | cos((M_PI * $x * $i) / $width) * 120 | cos((M_PI * $y * $j) / $height); 121 | 122 | $r += $color[0] * $basis; 123 | $g += $color[1] * $basis; 124 | $b += $color[2] * $basis; 125 | } 126 | } 127 | 128 | $row[] = $linear ? [$r, $g, $b] : [ 129 | Color::toSRGB($r), 130 | Color::toSRGB($g), 131 | Color::toSRGB($b) 132 | ]; 133 | } 134 | $pixels[] = $row; 135 | } 136 | 137 | return $pixels; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Color.php: -------------------------------------------------------------------------------- 1 | > 16; 16 | $g = ($value >> 8) & 255; 17 | $b = $value & 255; 18 | return [ 19 | Color::toLinear($r), 20 | Color::toLinear($g), 21 | Color::toLinear($b) 22 | ]; 23 | } 24 | } -------------------------------------------------------------------------------- /test/ACTest.php: -------------------------------------------------------------------------------- 1 | assertSame (3429.0, AC::encode ([0, 0, 0], 1)); 11 | $this->assertSame (6858.0, AC::encode ([255, 255, 255], 1)); 12 | $this->assertSame (0.0, AC::encode ([-1, -1, -1], 1)); 13 | } 14 | 15 | public function testDecode () { 16 | $this->assertSame ([0.0, 0.0, 0.0], AC::decode (3429, 1)); 17 | $this->assertSame ([1.0, 1.0, 1.0], AC::decode (6858, 1)); 18 | $this->assertSame ([-1.0, -1.0, -1.0], AC::decode (0, 1)); 19 | } 20 | } -------------------------------------------------------------------------------- /test/Base83Test.php: -------------------------------------------------------------------------------- 1 | assertEquals ($expected, Base83::encode($value, $length)); 18 | } 19 | } 20 | 21 | public function testEncodeThrows (): void { 22 | $this->expectException(InvalidArgumentException::class); 23 | $this->expectExceptionMessage('Specified length is too short to encode given value.'); 24 | Base83::encode (PHP_INT_MAX, 1); 25 | } 26 | 27 | /** 28 | * @dataProvider data 29 | */ 30 | public function testDecode ($length, $tests): void { 31 | foreach ($tests as $test) { 32 | $value = $test[1]; 33 | $expected = $test[0]; 34 | $this->assertEquals ($expected, Base83::decode($value)); 35 | } 36 | } 37 | 38 | public static function data(): array { 39 | return [ 40 | [1, [ 41 | [0, '0'], [1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5'], [6, '6'], [7, '7'], [8, '8'], [9, '9'], 42 | [10, 'A'], [11, 'B'], [12, 'C'], [13, 'D'], [14, 'E'], [15, 'F'], [16, 'G'], [17, 'H'], [18, 'I'], [19, 'J'], 43 | [20, 'K'], [21, 'L'], [22, 'M'], [23, 'N'], [24, 'O'], [25, 'P'], [26, 'Q'], [27, 'R'], [28, 'S'], [29, 'T'], 44 | [30, 'U'], [31, 'V'], [32, 'W'], [33, 'X'], [34, 'Y'], [35, 'Z'], [36, 'a'], [37, 'b'], [38, 'c'], [39, 'd'], 45 | [40, 'e'], [41, 'f'], [42, 'g'], [43, 'h'], [44, 'i'], [45, 'j'], [46, 'k'], [47, 'l'], [48, 'm'], [49, 'n'], 46 | [50, 'o'], [51, 'p'], [52, 'q'], [53, 'r'], [54, 's'], [55, 't'], [56, 'u'], [57, 'v'], [58, 'w'], [59, 'x'], 47 | [60, 'y'], [61, 'z'], [62, '#'], [63, '$'], [64, '%'], [65, '*'], [66, '+'], [67, ','], [68, '-'], [69, '.'], 48 | [70, ':'], [71, ';'], [72, '='], [73, '?'], [74, '@'], [75, '['], [76, ']'], [77, '^'], [78, '_'], [79, '{'], 49 | [80, '|'], [81, '}'], [82, '~'], 50 | ]], 51 | [2, [ 52 | [83, '10'], [84, '11'], [85, '12'], [86, '13'], [87, '14'], [88, '15'], [89, '16'], 53 | [90, '17'], [91, '18'], [92, '19'], [93, '1A'], [94, '1B'], [95, '1C'], [96, '1D'], [97, '1E'], [98, '1F'], [99, '1G'], 54 | [100, '1H'], [101, '1I'], [102, '1J'], [103, '1K'], [104, '1L'], [105, '1M'], [106, '1N'], [107, '1O'], [108, '1P'], [109, '1Q'], 55 | [110, '1R'], [111, '1S'], [112, '1T'], [113, '1U'], [114, '1V'], [115, '1W'], [116, '1X'], [117, '1Y'], [118, '1Z'], [119, '1a'], 56 | [120, '1b'], [121, '1c'], [122, '1d'], [123, '1e'], [124, '1f'], [125, '1g'], [126, '1h'], [127, '1i'], [128, '1j'], [129, '1k'], 57 | [130, '1l'], [131, '1m'], [132, '1n'], [133, '1o'], [134, '1p'], [135, '1q'], [136, '1r'], [137, '1s'], [138, '1t'], [139, '1u'], 58 | [140, '1v'], [141, '1w'], [142, '1x'], [143, '1y'], [144, '1z'], [145, '1#'], [146, '1$'], [147, '1%'], [148, '1*'], [149, '1+'], 59 | [150, '1,'], [151, '1-'], [152, '1.'], [153, '1:'], [154, '1;'], [155, '1='], [156, '1?'], [157, '1@'], [158, '1['], [159, '1]'], 60 | [160, '1^'], [161, '1_'], [162, '1{'], [163, '1|'], [164, '1}'], [165, '1~'], 61 | [166, '20'] 62 | ]], 63 | ]; 64 | } 65 | } -------------------------------------------------------------------------------- /test/BlurhashTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 13 | $this->expectExceptionMessage('x and y component counts must be between 1 and 9 inclusive.'); 14 | Blurhash::encode([], -1); 15 | Blurhash::encode([], -1, 5); 16 | Blurhash::encode([], -1, 17); 17 | } 18 | 19 | public function testDecodeThrowsShortHash() { 20 | $this->expectException(InvalidArgumentException::class); 21 | $this->expectExceptionMessage('Blurhash string must be at least 6 characters'); 22 | Blurhash::decode('', 0, 0); 23 | } 24 | 25 | /** 26 | * @dataProvider hashes 27 | */ 28 | public function testDecodeThrowsWrongSize ($hash, $width, $height) { 29 | $this->expectException(InvalidArgumentException::class); 30 | $length = strlen ($hash); 31 | $provided = $length - 1; 32 | $this->expectExceptionMessage("Blurhash length mismatch: length is {$provided} but it should be {$length}"); 33 | Blurhash::decode(substr ($hash, 0, -1), $width, $height); 34 | } 35 | 36 | /** 37 | * @dataProvider hashes 38 | */ 39 | public function testDecode($hash, $width, $height) { 40 | $decoded = Blurhash::decode($hash, $width, $height); 41 | 42 | $filename = sprintf ('%s/data/%s.json', __DIR__, substr($hash, 0, 4)); 43 | $data = file_get_contents($filename); 44 | $this->assertIsArray($decoded); 45 | $this->assertSame (json_decode ($data, true), $decoded); 46 | } 47 | 48 | public function hashes(): array { 49 | return [ 50 | ['LEHV9uae2yk8pyo0adR*.7kCMdnj', 269, 173], 51 | ['LGFO~6Yk^6#M@-5c,1Ex@@or[j6o', 301, 193], 52 | ['L6Pj42nh.AyE?vt7t7R**0o#DgR4', 242, 172], 53 | ['LKO2?V%2Tw=^]~RBVZRi};RPxuwH', 187, 120], 54 | ]; 55 | } 56 | 57 | /** 58 | * @dataProvider imageFiles 59 | */ 60 | public function testEncode($image, $hash) { 61 | $pixels = $this->getImagePixels($image); 62 | $this->assertSame($hash, Blurhash::encode ($pixels, 4, 3)); 63 | } 64 | 65 | public function imageFiles(): array { 66 | return [ 67 | [__DIR__ . '/data/img1.jpg', 'LEHV9uae2yk8pyo0adR*.7kCMdnj'], 68 | [__DIR__ . '/data/img2.jpg', 'LGFO~6Yk^6#M@-5c,1Ex@@or[j6o'], 69 | [__DIR__ . '/data/img3.jpg', 'L6Pj42nh.AyE?vt7t7R**0o#DgR4'], 70 | [__DIR__ . '/data/img4.jpg', 'LKO2?V%2Tw=^]~RBVZRi};RPxuwH'], 71 | ]; 72 | } 73 | 74 | private function getImagePixels($file) { 75 | $image = imagecreatefromstring(file_get_contents($file)); 76 | $width = imagesx($image); 77 | $height = imagesy($image); 78 | 79 | $pixels = []; 80 | for ($y = 0; $y < $height; ++$y) { 81 | $row = []; 82 | for ($x = 0; $x < $width; ++$x) { 83 | $index = imagecolorat($image, $x, $y); 84 | $colors = imagecolorsforindex($image, $index); 85 | 86 | $row[] = [$colors['red'], $colors['green'], $colors['blue']]; 87 | } 88 | 89 | $pixels[] = $row; 90 | } 91 | return $pixels; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/DCTest.php: -------------------------------------------------------------------------------- 1 | assertSame (65793, DC::encode ([0, 0, 0], 1)); 11 | $this->assertSame (16777215, DC::encode ([255, 255, 255], 1)); 12 | $this->assertSame (65793, DC::encode ([-1, -1, -1], 1)); 13 | } 14 | 15 | public function testDecode () { 16 | $this->assertSame ([0.0, 0.004024717018496307, 0.1301364766903643], DC::decode (3429, 1)); 17 | $this->assertSame ([0.0, 0.010329823029626936, 0.5906188409193369], DC::decode (6858, 1)); 18 | $this->assertSame ([0.0, 0.0, 0.0], DC::decode (0, 1)); 19 | } 20 | } -------------------------------------------------------------------------------- /test/data/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kornrunner/php-blurhash/fba428101ba32e72aab8e0b023fc31c9a67e3754/test/data/img1.jpg -------------------------------------------------------------------------------- /test/data/img2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kornrunner/php-blurhash/fba428101ba32e72aab8e0b023fc31c9a67e3754/test/data/img2.jpg -------------------------------------------------------------------------------- /test/data/img3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kornrunner/php-blurhash/fba428101ba32e72aab8e0b023fc31c9a67e3754/test/data/img3.jpg -------------------------------------------------------------------------------- /test/data/img4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kornrunner/php-blurhash/fba428101ba32e72aab8e0b023fc31c9a67e3754/test/data/img4.jpg --------------------------------------------------------------------------------