├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── blurhash ├── __init__.py └── blurhash.py ├── blurhash_example.png ├── cool_cat_small.jpg ├── example.py ├── setup.cfg ├── setup.py └── tests ├── blurhash_out.npy ├── cool_cat.jpg └── test_blurhash.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lorenz Diener 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tests * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blurhash-python 2 | ```python 3 | import blurhash 4 | import PIL.Image 5 | import numpy 6 | 7 | PIL.Image.open("cool_cat_small.jpg") 8 | # Result: 9 | ``` 10 | ![A picture of a cool cat.](/cool_cat_small.jpg?raw=true "A cool cat.") 11 | ```python 12 | blurhash.encode(numpy.array(PIL.Image.open("cool_cat_small.jpg").convert("RGB"))) 13 | # Result: 'UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH' 14 | 15 | PIL.Image.fromarray(numpy.array(blurhash.decode('UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH', 128, 128)).astype('uint8')) 16 | # Result: 17 | ``` 18 | ![Blurhash example output: A blurred cool cat.](/blurhash_example.png?raw=true "Blurhash example output: A blurred cool cat.") 19 | 20 | Blurhash is an algorithm that lets you transform image data into a small text representation of a blurred version of the image. This is useful since this small textual representation can be included when sending objects that may have images attached around, which then can be used to quickly create a placeholder for images that are still loading or that should be hidden behind a content warning. 21 | 22 | This library contains a pure-python implementation of the blurhash algorithm, closely following the original swift implementation by Dag Ågren. The module has no dependencies (the unit tests require PIL and numpy). You can install it via pip: 23 | 24 | ```bash 25 | $ pip3 install blurhash 26 | ``` 27 | 28 | It exports five functions: 29 | * "encode" and "decode" do the actual en- and decoding of blurhash strings 30 | * "components" returns the number of components x- and y components of a blurhash 31 | * "srgb_to_linear" and "linear_to_srgb" are colour space conversion helpers 32 | 33 | Have a look at example.py for an example of how to use all of these working together. 34 | 35 | Documentation for each function: 36 | 37 | ```python 38 | blurhash.encode(image, components_x = 4, components_y = 4, linear = False): 39 | """ 40 | Calculates the blurhash for an image using the given x and y component counts. 41 | 42 | Image should be a 3-dimensional array, with the first dimension being y, the second 43 | being x, and the third being the three rgb components that are assumed to be 0-255 44 | srgb integers (incidentally, this is the format you will get from a PIL RGB image). 45 | 46 | You can also pass in already linear data - to do this, set linear to True. This is 47 | useful if you want to encode a version of your image resized to a smaller size (which 48 | you should ideally do in linear colour). 49 | """ 50 | 51 | blurhash.decode(blurhash, width, height, punch = 1.0, linear = False) 52 | """ 53 | Decodes the given blurhash to an image of the specified size. 54 | 55 | Returns the resulting image a list of lists of 3-value sRGB 8 bit integer 56 | lists. Set linear to True if you would prefer to get linear floating point 57 | RGB back. 58 | 59 | The punch parameter can be used to de- or increase the contrast of the 60 | resulting image. 61 | 62 | As per the original implementation it is suggested to only decode 63 | to a relatively small size and then scale the result up, as it 64 | basically looks the same anyways. 65 | """ 66 | 67 | blurhash.srgb_to_linear(value): 68 | """ 69 | srgb 0-255 integer to linear 0.0-1.0 floating point conversion. 70 | """ 71 | 72 | blurhash.linear_to_srgb(value): 73 | """ 74 | linear 0.0-1.0 floating point to srgb 0-255 integer conversion. 75 | """ 76 | ``` 77 | -------------------------------------------------------------------------------- /blurhash/__init__.py: -------------------------------------------------------------------------------- 1 | from .blurhash import blurhash_encode as encode 2 | from .blurhash import blurhash_decode as decode 3 | from .blurhash import blurhash_components as components 4 | from .blurhash import srgb_to_linear as srgb_to_linear 5 | from .blurhash import linear_to_srgb as linear_to_srgb 6 | 7 | __all__ = ['encode', 'decode', 'components', 'srgb_to_linear', 'linear_to_srgb'] 8 | -------------------------------------------------------------------------------- /blurhash/blurhash.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pure python blurhash decoder with no additional dependencies, for 3 | both de- and encoding. 4 | 5 | Very close port of the original Swift implementation by Dag Ågren. 6 | """ 7 | 8 | import math 9 | 10 | # Alphabet for base 83 11 | alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" 12 | alphabet_values = dict(zip(alphabet, range(len(alphabet)))) 13 | 14 | def base83_decode(base83_str): 15 | """ 16 | Decodes a base83 string, as used in blurhash, to an integer. 17 | """ 18 | value = 0 19 | for base83_char in base83_str: 20 | value = value * 83 + alphabet_values[base83_char] 21 | return value 22 | 23 | def base83_encode(value, length): 24 | """ 25 | Decodes an integer to a base83 string, as used in blurhash. 26 | 27 | Length is how long the resulting string should be. Will complain 28 | if the specified length is too short. 29 | """ 30 | if int(value) // (83 ** (length)) != 0: 31 | raise ValueError("Specified length is too short to encode given value.") 32 | 33 | result = "" 34 | for i in range(1, length + 1): 35 | digit = int(value) // (83 ** (length - i)) % 83 36 | result += alphabet[int(digit)] 37 | return result 38 | 39 | def srgb_to_linear(value): 40 | """ 41 | srgb 0-255 integer to linear 0.0-1.0 floating point conversion. 42 | """ 43 | value = float(value) / 255.0 44 | if value <= 0.04045: 45 | return value / 12.92 46 | return math.pow((value + 0.055) / 1.055, 2.4) 47 | 48 | def sign_pow(value, exp): 49 | """ 50 | Sign-preserving exponentiation. 51 | """ 52 | return math.copysign(math.pow(abs(value), exp), value) 53 | 54 | def linear_to_srgb(value): 55 | """ 56 | linear 0.0-1.0 floating point to srgb 0-255 integer conversion. 57 | """ 58 | value = max(0.0, min(1.0, value)) 59 | if value <= 0.0031308: 60 | return int(value * 12.92 * 255 + 0.5) 61 | return int((1.055 * math.pow(value, 1 / 2.4) - 0.055) * 255 + 0.5) 62 | 63 | def blurhash_components(blurhash): 64 | """ 65 | Decodes and returns the number of x and y components in the given blurhash. 66 | """ 67 | if len(blurhash) < 6: 68 | raise ValueError("BlurHash must be at least 6 characters long.") 69 | 70 | # Decode metadata 71 | size_info = base83_decode(blurhash[0]) 72 | size_y = int(size_info / 9) + 1 73 | size_x = (size_info % 9) + 1 74 | 75 | return size_x, size_y 76 | 77 | def blurhash_decode(blurhash, width, height, punch = 1.0, linear = False): 78 | """ 79 | Decodes the given blurhash to an image of the specified size. 80 | 81 | Returns the resulting image a list of lists of 3-value sRGB 8 bit integer 82 | lists. Set linear to True if you would prefer to get linear floating point 83 | RGB back. 84 | 85 | The punch parameter can be used to de- or increase the contrast of the 86 | resulting image. 87 | 88 | As per the original implementation it is suggested to only decode 89 | to a relatively small size and then scale the result up, as it 90 | basically looks the same anyways. 91 | """ 92 | if len(blurhash) < 6: 93 | raise ValueError("BlurHash must be at least 6 characters long.") 94 | 95 | # Decode metadata 96 | size_info = base83_decode(blurhash[0]) 97 | size_y = int(size_info / 9) + 1 98 | size_x = (size_info % 9) + 1 99 | 100 | quant_max_value = base83_decode(blurhash[1]) 101 | real_max_value = (float(quant_max_value + 1) / 166.0) * punch 102 | 103 | # Make sure we at least have the right number of characters 104 | if len(blurhash) != 4 + 2 * size_x * size_y: 105 | raise ValueError("Invalid BlurHash length.") 106 | 107 | # Decode DC component 108 | dc_value = base83_decode(blurhash[2:6]) 109 | colours = [( 110 | srgb_to_linear(dc_value >> 16), 111 | srgb_to_linear((dc_value >> 8) & 255), 112 | srgb_to_linear(dc_value & 255) 113 | )] 114 | 115 | # Decode AC components 116 | for component in range(1, size_x * size_y): 117 | ac_value = base83_decode(blurhash[4+component*2:4+(component+1)*2]) 118 | colours.append(( 119 | sign_pow((float(int(ac_value / (19 * 19))) - 9.0) / 9.0, 2.0) * real_max_value, 120 | sign_pow((float(int(ac_value / 19) % 19) - 9.0) / 9.0, 2.0) * real_max_value, 121 | sign_pow((float(ac_value % 19) - 9.0) / 9.0, 2.0) * real_max_value 122 | )) 123 | 124 | # Return image RGB values, as a list of lists of lists, 125 | # consumable by something like numpy or PIL. 126 | pixels = [] 127 | for y in range(height): 128 | pixel_row = [] 129 | for x in range(width): 130 | pixel = [0.0, 0.0, 0.0] 131 | 132 | for j in range(size_y): 133 | for i in range(size_x): 134 | basis = math.cos(math.pi * float(x) * float(i) / float(width)) * \ 135 | math.cos(math.pi * float(y) * float(j) / float(height)) 136 | colour = colours[i + j * size_x] 137 | pixel[0] += colour[0] * basis 138 | pixel[1] += colour[1] * basis 139 | pixel[2] += colour[2] * basis 140 | if linear == False: 141 | pixel_row.append([ 142 | linear_to_srgb(pixel[0]), 143 | linear_to_srgb(pixel[1]), 144 | linear_to_srgb(pixel[2]), 145 | ]) 146 | else: 147 | pixel_row.append(pixel) 148 | pixels.append(pixel_row) 149 | return pixels 150 | 151 | def blurhash_encode(image, components_x = 4, components_y = 4, linear = False): 152 | """ 153 | Calculates the blurhash for an image using the given x and y component counts. 154 | 155 | Image should be a 3-dimensional array, with the first dimension being y, the second 156 | being x, and the third being the three rgb components that are assumed to be 0-255 157 | srgb integers (incidentally, this is the format you will get from a PIL RGB image). 158 | 159 | You can also pass in already linear data - to do this, set linear to True. This is 160 | useful if you want to encode a version of your image resized to a smaller size (which 161 | you should ideally do in linear colour). 162 | """ 163 | if components_x < 1 or components_x > 9 or components_y < 1 or components_y > 9: 164 | raise ValueError("x and y component counts must be between 1 and 9 inclusive.") 165 | height = float(len(image)) 166 | width = float(len(image[0])) 167 | 168 | # Convert to linear if neeeded 169 | image_linear = [] 170 | if linear == False: 171 | for y in range(int(height)): 172 | image_linear_line = [] 173 | for x in range(int(width)): 174 | image_linear_line.append([ 175 | srgb_to_linear(image[y][x][0]), 176 | srgb_to_linear(image[y][x][1]), 177 | srgb_to_linear(image[y][x][2]) 178 | ]) 179 | image_linear.append(image_linear_line) 180 | else: 181 | image_linear = image 182 | 183 | # Calculate components 184 | components = [] 185 | max_ac_component = 0.0 186 | for j in range(components_y): 187 | for i in range(components_x): 188 | norm_factor = 1.0 if (i == 0 and j == 0) else 2.0 189 | component = [0.0, 0.0, 0.0] 190 | for y in range(int(height)): 191 | for x in range(int(width)): 192 | basis = norm_factor * math.cos(math.pi * float(i) * float(x) / width) * \ 193 | math.cos(math.pi * float(j) * float(y) / height) 194 | component[0] += basis * image_linear[y][x][0] 195 | component[1] += basis * image_linear[y][x][1] 196 | component[2] += basis * image_linear[y][x][2] 197 | 198 | component[0] /= (width * height) 199 | component[1] /= (width * height) 200 | component[2] /= (width * height) 201 | components.append(component) 202 | 203 | if not (i == 0 and j == 0): 204 | max_ac_component = max(max_ac_component, abs(component[0]), abs(component[1]), abs(component[2])) 205 | 206 | # Encode components 207 | dc_value = (linear_to_srgb(components[0][0]) << 16) + \ 208 | (linear_to_srgb(components[0][1]) << 8) + \ 209 | linear_to_srgb(components[0][2]) 210 | 211 | quant_max_ac_component = int(max(0, min(82, math.floor(max_ac_component * 166 - 0.5)))) 212 | ac_component_norm_factor = float(quant_max_ac_component + 1) / 166.0 213 | 214 | ac_values = [] 215 | for r, g, b in components[1:]: 216 | ac_values.append( 217 | int(max(0.0, min(18.0, math.floor(sign_pow(r / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) * 19 * 19 + \ 218 | int(max(0.0, min(18.0, math.floor(sign_pow(g / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) * 19 + \ 219 | int(max(0.0, min(18.0, math.floor(sign_pow(b / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) 220 | ) 221 | 222 | # Build final blurhash 223 | blurhash = "" 224 | blurhash += base83_encode((components_x - 1) + (components_y - 1) * 9, 1) 225 | blurhash += base83_encode(quant_max_ac_component, 1) 226 | blurhash += base83_encode(dc_value, 4) 227 | for ac_value in ac_values: 228 | blurhash += base83_encode(ac_value, 2) 229 | 230 | return blurhash 231 | -------------------------------------------------------------------------------- /blurhash_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halcy/blurhash-python/6e60daa0607922499a421f51b4301eea759eda6a/blurhash_example.png -------------------------------------------------------------------------------- /cool_cat_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halcy/blurhash-python/6e60daa0607922499a421f51b4301eea759eda6a/cool_cat_small.jpg -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example "proper" blurhash usage 3 | """ 4 | 5 | import numpy as np 6 | import PIL.Image 7 | import blurhash 8 | 9 | # Input/output file names 10 | input_image = "tests/cool_cat.jpg" 11 | output_image = "example_out.png" 12 | 13 | # How many components do we want, maximum and minimum? 14 | target_components = 4 15 | min_components = 2 16 | 17 | # Which maximum size should we work at for blurhash calculations? 18 | work_size = 64 19 | 20 | # What's the final intended output size? 21 | out_size = (320, 180) 22 | 23 | """ 24 | Part 1: Encode 25 | """ 26 | # Load the image and store sizes (useful for decoding later, and likely part of your 27 | # metadata objects anyways) 28 | image = PIL.Image.open(input_image).convert("RGB") 29 | image_size = (image.width, image.height) 30 | print("Read image " + input_image + "({} x {})".format(image_size[0], image_size[1])) 31 | 32 | # Convert to linear and thumbnail 33 | image_linear = np.vectorize(blurhash.srgb_to_linear)(np.array(image)) 34 | image_linear_thumb = [] 35 | for i in range(3): 36 | channel_linear = PIL.Image.fromarray(image_linear[:,:,i].astype("float32"), mode = 'F') 37 | channel_linear.thumbnail((work_size, work_size)) 38 | image_linear_thumb.append(np.array(channel_linear)) 39 | image_linear_thumb = np.transpose(np.array(image_linear_thumb), (1, 2, 0)) 40 | print("Encoder working at size: {} x {}".format(image_linear_thumb.shape[1], image_linear_thumb.shape[0])) 41 | 42 | # Figure out a good component count 43 | components_x = int(max(min_components, min(target_components, round(image_linear_thumb.shape[1] / (work_size / target_components))))) 44 | components_y = int(max(min_components, min(target_components, round(image_linear_thumb.shape[0] / (work_size / target_components))))) 45 | print("Using component counts: {} x {}".format(components_x, components_y)) 46 | 47 | # Create blurhash 48 | blur_hash = blurhash.encode(image_linear_thumb, components_x, components_y, linear = True) 49 | print("Blur hash of image: " + blur_hash) 50 | 51 | """ 52 | Part 2: Decode 53 | """ 54 | # Figure out what size to decode to 55 | decode_components_x, decode_components_y = blurhash.components(blur_hash) 56 | decode_size_x = decode_components_x * (work_size // target_components) 57 | decode_size_y = decode_components_y * (work_size // target_components) 58 | print("Decoder working at size {} x {}".format(decode_size_x, decode_size_y)) 59 | 60 | # Decode 61 | decoded_image = np.array(blurhash.decode(blur_hash, decode_size_x, decode_size_y, linear = True)) 62 | 63 | # Scale so that we have the right size to fill out_size without letter/pillarboxing 64 | # while matching original images aspect ratio. 65 | fill_x_size_y = out_size[0] * (image_size[0] / image_size[1]) 66 | fill_y_size_x = out_size[1] * (image_size[1] / image_size[0]) 67 | scale_target_size = list(out_size) 68 | if fill_x_size_y / out_size[1] < fill_y_size_x / out_size[0]: 69 | scale_target_size[0] = max(scale_target_size[0], int(fill_y_size_x)) 70 | else: 71 | scale_target_size[1] = max(scale_target_size[1], int(fill_x_size_y)) 72 | 73 | # Scale (ideally, your UI layer should take care of this in some kind of efficient way) 74 | print("Scaling to target size: {} x {}".format(scale_target_size[0], scale_target_size[1])) 75 | decoded_image_large = [] 76 | for i in range(3): 77 | channel_linear = PIL.Image.fromarray(decoded_image[:,:,i].astype("float32"), mode = 'F') 78 | decoded_image_large.append(np.array(channel_linear.resize(scale_target_size, PIL.Image.BILINEAR))) 79 | decoded_image_large = np.transpose(np.array(decoded_image_large), (1, 2, 0)) 80 | 81 | # Convert to srgb PIL image 82 | decoded_image_out = np.vectorize(blurhash.linear_to_srgb)(np.array(decoded_image_large)) 83 | decoded_image_out = PIL.Image.fromarray(np.array(decoded_image_out).astype('uint8')) 84 | 85 | # Crop to final size and write 86 | decoded_image_out = decoded_image_out.crop(( 87 | (decoded_image_out.width - out_size[0]) / 2, 88 | (decoded_image_out.height - out_size[1]) / 2, 89 | (decoded_image_out.width + out_size[0]) / 2, 90 | (decoded_image_out.height + out_size[1]) / 2, 91 | )) 92 | decoded_image_out.save(output_image) 93 | print("Wrote final result to " + str(output_image)) 94 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [aliases] 5 | test=pytest 6 | 7 | [tool:pytest] 8 | addopts = --cov=blurhash 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | test_deps = ['pytest', 'Pillow', 'numpy'] 4 | extras = { 5 | "test": test_deps 6 | } 7 | 8 | setup(name='blurhash', 9 | version='1.1.4', 10 | description='Pure-Python implementation of the blurhash algorithm.', 11 | packages=['blurhash'], 12 | install_requires=[], 13 | tests_require=test_deps, 14 | extras_require=extras, 15 | url='https://github.com/halcy/blurhash-python', 16 | author='Lorenz Diener', 17 | author_email='lorenzd+blurhashpypi@gmail.com', 18 | license='MIT', 19 | keywords='blurhash graphics web_development', 20 | classifiers=[ 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Intended Audience :: Developers', 23 | 'Topic :: Multimedia :: Graphics', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 3', 27 | ]) 28 | -------------------------------------------------------------------------------- /tests/blurhash_out.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halcy/blurhash-python/6e60daa0607922499a421f51b4301eea759eda6a/tests/blurhash_out.npy -------------------------------------------------------------------------------- /tests/cool_cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halcy/blurhash-python/6e60daa0607922499a421f51b4301eea759eda6a/tests/cool_cat.jpg -------------------------------------------------------------------------------- /tests/test_blurhash.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | base_path = os.path.dirname(os.path.abspath(__file__)) 3 | sys.path.insert(0, os.path.join(base_path, '..')) 4 | 5 | import PIL 6 | import PIL.Image 7 | import blurhash 8 | import numpy as np 9 | import pytest 10 | 11 | def test_encode(): 12 | image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) 13 | blur_hash = blurhash.encode(np.array(image.convert("RGB"))) 14 | assert blur_hash == "UBMOZfK1GG%LBBNG,;Rj2skq=eE1s9n4S5Na" 15 | 16 | def test_decode(): 17 | image = blurhash.decode("UBMOZfK1GG%LBBNG,;Rj2skq=eE1s9n4S5Na", 32, 32) 18 | reference_image = np.load(os.path.join(base_path, "blurhash_out.npy")) 19 | assert np.sum(np.abs(image - reference_image)) < 1.0 20 | 21 | def test_asymmetric(): 22 | image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) 23 | blur_hash = blurhash.encode(np.array(image.convert("RGB")), components_x = 2, components_y = 8) 24 | assert blur_hash == "%BMOZfK1BBNG2skqs9n4?HvgJ.Nav}J-$%sm" 25 | 26 | decoded_image = blurhash.decode(blur_hash, 32, 32) 27 | assert np.sum(np.var(decoded_image, axis = 0)) > np.sum(np.var(decoded_image, axis = 1)) 28 | 29 | blur_hash = blurhash.encode(np.array(image.convert("RGB")), components_x = 8, components_y = 2) 30 | decoded_image = blurhash.decode(blur_hash, 32, 32) 31 | assert np.sum(np.var(decoded_image, axis = 0)) < np.sum(np.var(decoded_image, axis = 1)) 32 | 33 | def test_components(): 34 | image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) 35 | blur_hash = blurhash.encode(np.array(image.convert("RGB")), components_x = 8, components_y = 3) 36 | size_x, size_y = blurhash.components(blur_hash) 37 | assert size_x == 8 38 | assert size_y == 3 39 | 40 | def test_linear_dc_only(): 41 | image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) 42 | linearish_image = np.array(image.convert("RGB")) / 255.0 43 | blur_hash = blurhash.encode(linearish_image, components_x = 1, components_y = 1, linear = True) 44 | avg_color = blurhash.decode(blur_hash, 1, 1, linear = True) 45 | reference_avg_color = np.mean(linearish_image.reshape(linearish_image.shape[0] * linearish_image.shape[1], -1), 0) 46 | assert np.sum(np.abs(avg_color - reference_avg_color)) < 0.01 47 | 48 | def test_invalid_parameters(): 49 | image = np.array(PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")).convert("RGB")) 50 | 51 | with pytest.raises(ValueError): 52 | blurhash.decode("UBMO", 32, 32) 53 | 54 | with pytest.raises(ValueError): 55 | blurhash.decode("UBMOZfK1GG%LBBNG", 32, 32) 56 | 57 | with pytest.raises(ValueError): 58 | blurhash.encode(image, components_x = 0, components_y = 1) 59 | 60 | with pytest.raises(ValueError): 61 | blurhash.encode(image, components_x = 1, components_y = 0) 62 | 63 | with pytest.raises(ValueError): 64 | blurhash.encode(image, components_x = 1, components_y = 10) 65 | 66 | with pytest.raises(ValueError): 67 | blurhash.encode(image, components_x = 10, components_y = 1) 68 | --------------------------------------------------------------------------------