././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1755426957.4475205 blurhash-1.1.5/0000755000175100001660000000000015050330215012723 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/LICENSE0000644000175100001660000000205615050330202013727 0ustar00runnerdockerMIT License Copyright (c) 2019 Lorenz Diener Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/MANIFEST.in0000644000175100001660000000010415050330202014450 0ustar00runnerdockerrecursive-include tests * recursive-exclude * __pycache__ *.py[cod] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1755426957.4475205 blurhash-1.1.5/PKG-INFO0000644000175100001660000000770415050330215014030 0ustar00runnerdockerMetadata-Version: 2.4 Name: blurhash Version: 1.1.5 Summary: Pure-Python implementation of the blurhash algorithm. Home-page: https://github.com/halcy/blurhash-python Author: Lorenz Diener Author-email: lorenzd+blurhashpypi@gmail.com License: MIT Keywords: blurhash graphics web_development Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Multimedia :: Graphics Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Description-Content-Type: text/markdown License-File: LICENSE Provides-Extra: test Requires-Dist: pytest; extra == "test" Requires-Dist: Pillow; extra == "test" Requires-Dist: numpy; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: provides-extra Dynamic: summary # blurhash-python ```python import blurhash import PIL.Image import numpy PIL.Image.open("cool_cat_small.jpg") # Result: ``` ![A picture of a cool cat.](/cool_cat_small.jpg?raw=true "A cool cat.") ```python blurhash.encode(numpy.array(PIL.Image.open("cool_cat_small.jpg").convert("RGB"))) # Result: 'UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH' PIL.Image.fromarray(numpy.array(blurhash.decode('UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH', 128, 128)).astype('uint8')) # Result: ``` ![Blurhash example output: A blurred cool cat.](/blurhash_example.png?raw=true "Blurhash example output: A blurred cool cat.") 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. 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: ```bash $ pip3 install blurhash ``` It exports five functions: * "encode" and "decode" do the actual en- and decoding of blurhash strings * "components" returns the number of components x- and y components of a blurhash * "srgb_to_linear" and "linear_to_srgb" are colour space conversion helpers Have a look at example.py for an example of how to use all of these working together. Documentation for each function: ```python blurhash.encode(image, components_x = 4, components_y = 4, linear = False): """ Calculates the blurhash for an image using the given x and y component counts. Image should be a 3-dimensional array, with the first dimension being y, the second being x, and the third being the three rgb components that are assumed to be 0-255 srgb integers (incidentally, this is the format you will get from a PIL RGB image). You can also pass in already linear data - to do this, set linear to True. This is useful if you want to encode a version of your image resized to a smaller size (which you should ideally do in linear colour). """ blurhash.decode(blurhash, width, height, punch = 1.0, linear = False) """ Decodes the given blurhash to an image of the specified size. Returns the resulting image a list of lists of 3-value sRGB 8 bit integer lists. Set linear to True if you would prefer to get linear floating point RGB back. The punch parameter can be used to de- or increase the contrast of the resulting image. As per the original implementation it is suggested to only decode to a relatively small size and then scale the result up, as it basically looks the same anyways. """ blurhash.srgb_to_linear(value): """ srgb 0-255 integer to linear 0.0-1.0 floating point conversion. """ blurhash.linear_to_srgb(value): """ linear 0.0-1.0 floating point to srgb 0-255 integer conversion. """ ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/README.md0000644000175100001660000000600715050330202014201 0ustar00runnerdocker# blurhash-python ```python import blurhash import PIL.Image import numpy PIL.Image.open("cool_cat_small.jpg") # Result: ``` ![A picture of a cool cat.](/cool_cat_small.jpg?raw=true "A cool cat.") ```python blurhash.encode(numpy.array(PIL.Image.open("cool_cat_small.jpg").convert("RGB"))) # Result: 'UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH' PIL.Image.fromarray(numpy.array(blurhash.decode('UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH', 128, 128)).astype('uint8')) # Result: ``` ![Blurhash example output: A blurred cool cat.](/blurhash_example.png?raw=true "Blurhash example output: A blurred cool cat.") 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. 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: ```bash $ pip3 install blurhash ``` It exports five functions: * "encode" and "decode" do the actual en- and decoding of blurhash strings * "components" returns the number of components x- and y components of a blurhash * "srgb_to_linear" and "linear_to_srgb" are colour space conversion helpers Have a look at example.py for an example of how to use all of these working together. Documentation for each function: ```python blurhash.encode(image, components_x = 4, components_y = 4, linear = False): """ Calculates the blurhash for an image using the given x and y component counts. Image should be a 3-dimensional array, with the first dimension being y, the second being x, and the third being the three rgb components that are assumed to be 0-255 srgb integers (incidentally, this is the format you will get from a PIL RGB image). You can also pass in already linear data - to do this, set linear to True. This is useful if you want to encode a version of your image resized to a smaller size (which you should ideally do in linear colour). """ blurhash.decode(blurhash, width, height, punch = 1.0, linear = False) """ Decodes the given blurhash to an image of the specified size. Returns the resulting image a list of lists of 3-value sRGB 8 bit integer lists. Set linear to True if you would prefer to get linear floating point RGB back. The punch parameter can be used to de- or increase the contrast of the resulting image. As per the original implementation it is suggested to only decode to a relatively small size and then scale the result up, as it basically looks the same anyways. """ blurhash.srgb_to_linear(value): """ srgb 0-255 integer to linear 0.0-1.0 floating point conversion. """ blurhash.linear_to_srgb(value): """ linear 0.0-1.0 floating point to srgb 0-255 integer conversion. """ ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1755426957.4455204 blurhash-1.1.5/blurhash/0000755000175100001660000000000015050330215014533 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/blurhash/__init__.py0000644000175100001660000000053015050330202016636 0ustar00runnerdockerfrom .blurhash import blurhash_encode as encode from .blurhash import blurhash_decode as decode from .blurhash import blurhash_components as components from .blurhash import srgb_to_linear as srgb_to_linear from .blurhash import linear_to_srgb as linear_to_srgb __all__ = ['encode', 'decode', 'components', 'srgb_to_linear', 'linear_to_srgb'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/blurhash/blurhash.py0000644000175100001660000002103215050330202016707 0ustar00runnerdocker""" Pure python blurhash decoder with no additional dependencies, for both de- and encoding. Very close port of the original Swift implementation by Dag Ågren. """ import math # Alphabet for base 83 alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" alphabet_values = dict(zip(alphabet, range(len(alphabet)))) def base83_decode(base83_str): """ Decodes a base83 string, as used in blurhash, to an integer. """ value = 0 for base83_char in base83_str: value = value * 83 + alphabet_values[base83_char] return value def base83_encode(value, length): """ Decodes an integer to a base83 string, as used in blurhash. Length is how long the resulting string should be. Will complain if the specified length is too short. """ if int(value) // (83 ** (length)) != 0: raise ValueError("Specified length is too short to encode given value.") result = "" for i in range(1, length + 1): digit = int(value) // (83 ** (length - i)) % 83 result += alphabet[int(digit)] return result def srgb_to_linear(value): """ srgb 0-255 integer to linear 0.0-1.0 floating point conversion. """ value = float(value) / 255.0 if value <= 0.04045: return value / 12.92 return math.pow((value + 0.055) / 1.055, 2.4) def sign_pow(value, exp): """ Sign-preserving exponentiation. """ return math.copysign(math.pow(abs(value), exp), value) def linear_to_srgb(value): """ linear 0.0-1.0 floating point to srgb 0-255 integer conversion. """ value = max(0.0, min(1.0, value)) if value <= 0.0031308: return int(value * 12.92 * 255 + 0.5) return int((1.055 * math.pow(value, 1 / 2.4) - 0.055) * 255 + 0.5) def blurhash_components(blurhash): """ Decodes and returns the number of x and y components in the given blurhash. """ if len(blurhash) < 6: raise ValueError("BlurHash must be at least 6 characters long.") # Decode metadata size_info = base83_decode(blurhash[0]) size_y = int(size_info / 9) + 1 size_x = (size_info % 9) + 1 return size_x, size_y def blurhash_decode(blurhash, width, height, punch = 1.0, linear = False): """ Decodes the given blurhash to an image of the specified size. Returns the resulting image a list of lists of 3-value sRGB 8 bit integer lists. Set linear to True if you would prefer to get linear floating point RGB back. The punch parameter can be used to de- or increase the contrast of the resulting image. As per the original implementation it is suggested to only decode to a relatively small size and then scale the result up, as it basically looks the same anyways. """ if len(blurhash) < 6: raise ValueError("BlurHash must be at least 6 characters long.") # Decode metadata size_info = base83_decode(blurhash[0]) size_y = int(size_info / 9) + 1 size_x = (size_info % 9) + 1 quant_max_value = base83_decode(blurhash[1]) real_max_value = (float(quant_max_value + 1) / 166.0) * punch # Make sure we at least have the right number of characters if len(blurhash) != 4 + 2 * size_x * size_y: raise ValueError("Invalid BlurHash length.") # Decode DC component dc_value = base83_decode(blurhash[2:6]) colours = [( srgb_to_linear(dc_value >> 16), srgb_to_linear((dc_value >> 8) & 255), srgb_to_linear(dc_value & 255) )] # Decode AC components for component in range(1, size_x * size_y): ac_value = base83_decode(blurhash[4+component*2:4+(component+1)*2]) colours.append(( sign_pow((float(int(ac_value / (19 * 19))) - 9.0) / 9.0, 2.0) * real_max_value, sign_pow((float(int(ac_value / 19) % 19) - 9.0) / 9.0, 2.0) * real_max_value, sign_pow((float(ac_value % 19) - 9.0) / 9.0, 2.0) * real_max_value )) # Return image RGB values, as a list of lists of lists, # consumable by something like numpy or PIL. pixels = [] for y in range(height): pixel_row = [] for x in range(width): pixel = [0.0, 0.0, 0.0] for j in range(size_y): for i in range(size_x): basis = math.cos(math.pi * float(x) * float(i) / float(width)) * \ math.cos(math.pi * float(y) * float(j) / float(height)) colour = colours[i + j * size_x] pixel[0] += colour[0] * basis pixel[1] += colour[1] * basis pixel[2] += colour[2] * basis if linear == False: pixel_row.append([ linear_to_srgb(pixel[0]), linear_to_srgb(pixel[1]), linear_to_srgb(pixel[2]), ]) else: pixel_row.append(pixel) pixels.append(pixel_row) return pixels def blurhash_encode(image, components_x = 4, components_y = 4, linear = False): """ Calculates the blurhash for an image using the given x and y component counts. Image should be a 3-dimensional array, with the first dimension being y, the second being x, and the third being the three rgb components that are assumed to be 0-255 srgb integers (incidentally, this is the format you will get from a PIL RGB image). You can also pass in already linear data - to do this, set linear to True. This is useful if you want to encode a version of your image resized to a smaller size (which you should ideally do in linear colour). """ if components_x < 1 or components_x > 9 or components_y < 1 or components_y > 9: raise ValueError("x and y component counts must be between 1 and 9 inclusive.") height = float(len(image)) width = float(len(image[0])) # Convert to linear if neeeded image_linear = [] if linear == False: for y in range(int(height)): image_linear_line = [] for x in range(int(width)): image_linear_line.append([ srgb_to_linear(image[y][x][0]), srgb_to_linear(image[y][x][1]), srgb_to_linear(image[y][x][2]) ]) image_linear.append(image_linear_line) else: image_linear = image # Calculate components components = [] max_ac_component = 0.0 for j in range(components_y): for i in range(components_x): norm_factor = 1.0 if (i == 0 and j == 0) else 2.0 component = [0.0, 0.0, 0.0] for y in range(int(height)): for x in range(int(width)): basis = norm_factor * math.cos(math.pi * float(i) * float(x) / width) * \ math.cos(math.pi * float(j) * float(y) / height) component[0] += basis * image_linear[y][x][0] component[1] += basis * image_linear[y][x][1] component[2] += basis * image_linear[y][x][2] component[0] /= (width * height) component[1] /= (width * height) component[2] /= (width * height) components.append(component) if not (i == 0 and j == 0): max_ac_component = max(max_ac_component, abs(component[0]), abs(component[1]), abs(component[2])) # Encode components dc_value = (linear_to_srgb(components[0][0]) << 16) + \ (linear_to_srgb(components[0][1]) << 8) + \ linear_to_srgb(components[0][2]) quant_max_ac_component = int(max(0, min(82, math.floor(max_ac_component * 166 - 0.5)))) ac_component_norm_factor = float(quant_max_ac_component + 1) / 166.0 ac_values = [] for r, g, b in components[1:]: ac_values.append( int(max(0.0, min(18.0, math.floor(sign_pow(r / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) * 19 * 19 + \ int(max(0.0, min(18.0, math.floor(sign_pow(g / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) * 19 + \ int(max(0.0, min(18.0, math.floor(sign_pow(b / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) ) # Build final blurhash blurhash = "" blurhash += base83_encode((components_x - 1) + (components_y - 1) * 9, 1) blurhash += base83_encode(quant_max_ac_component, 1) blurhash += base83_encode(dc_value, 4) for ac_value in ac_values: blurhash += base83_encode(ac_value, 2) return blurhash ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1755426957.4465203 blurhash-1.1.5/blurhash.egg-info/0000755000175100001660000000000015050330215016225 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426957.0 blurhash-1.1.5/blurhash.egg-info/PKG-INFO0000644000175100001660000000770415050330215017332 0ustar00runnerdockerMetadata-Version: 2.4 Name: blurhash Version: 1.1.5 Summary: Pure-Python implementation of the blurhash algorithm. Home-page: https://github.com/halcy/blurhash-python Author: Lorenz Diener Author-email: lorenzd+blurhashpypi@gmail.com License: MIT Keywords: blurhash graphics web_development Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Multimedia :: Graphics Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Description-Content-Type: text/markdown License-File: LICENSE Provides-Extra: test Requires-Dist: pytest; extra == "test" Requires-Dist: Pillow; extra == "test" Requires-Dist: numpy; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: provides-extra Dynamic: summary # blurhash-python ```python import blurhash import PIL.Image import numpy PIL.Image.open("cool_cat_small.jpg") # Result: ``` ![A picture of a cool cat.](/cool_cat_small.jpg?raw=true "A cool cat.") ```python blurhash.encode(numpy.array(PIL.Image.open("cool_cat_small.jpg").convert("RGB"))) # Result: 'UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH' PIL.Image.fromarray(numpy.array(blurhash.decode('UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH', 128, 128)).astype('uint8')) # Result: ``` ![Blurhash example output: A blurred cool cat.](/blurhash_example.png?raw=true "Blurhash example output: A blurred cool cat.") 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. 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: ```bash $ pip3 install blurhash ``` It exports five functions: * "encode" and "decode" do the actual en- and decoding of blurhash strings * "components" returns the number of components x- and y components of a blurhash * "srgb_to_linear" and "linear_to_srgb" are colour space conversion helpers Have a look at example.py for an example of how to use all of these working together. Documentation for each function: ```python blurhash.encode(image, components_x = 4, components_y = 4, linear = False): """ Calculates the blurhash for an image using the given x and y component counts. Image should be a 3-dimensional array, with the first dimension being y, the second being x, and the third being the three rgb components that are assumed to be 0-255 srgb integers (incidentally, this is the format you will get from a PIL RGB image). You can also pass in already linear data - to do this, set linear to True. This is useful if you want to encode a version of your image resized to a smaller size (which you should ideally do in linear colour). """ blurhash.decode(blurhash, width, height, punch = 1.0, linear = False) """ Decodes the given blurhash to an image of the specified size. Returns the resulting image a list of lists of 3-value sRGB 8 bit integer lists. Set linear to True if you would prefer to get linear floating point RGB back. The punch parameter can be used to de- or increase the contrast of the resulting image. As per the original implementation it is suggested to only decode to a relatively small size and then scale the result up, as it basically looks the same anyways. """ blurhash.srgb_to_linear(value): """ srgb 0-255 integer to linear 0.0-1.0 floating point conversion. """ blurhash.linear_to_srgb(value): """ linear 0.0-1.0 floating point to srgb 0-255 integer conversion. """ ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426957.0 blurhash-1.1.5/blurhash.egg-info/SOURCES.txt0000644000175100001660000000047215050330215020114 0ustar00runnerdockerLICENSE MANIFEST.in README.md setup.cfg setup.py blurhash/__init__.py blurhash/blurhash.py blurhash.egg-info/PKG-INFO blurhash.egg-info/SOURCES.txt blurhash.egg-info/dependency_links.txt blurhash.egg-info/requires.txt blurhash.egg-info/top_level.txt tests/blurhash_out.npy tests/cool_cat.jpg tests/test_blurhash.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426957.0 blurhash-1.1.5/blurhash.egg-info/dependency_links.txt0000644000175100001660000000000115050330215022273 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426957.0 blurhash-1.1.5/blurhash.egg-info/requires.txt0000644000175100001660000000003415050330215020622 0ustar00runnerdocker [test] pytest Pillow numpy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426957.0 blurhash-1.1.5/blurhash.egg-info/top_level.txt0000644000175100001660000000001115050330215020747 0ustar00runnerdockerblurhash ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1755426957.4475205 blurhash-1.1.5/setup.cfg0000644000175100001660000000034115050330215014542 0ustar00runnerdocker[bdist_wheel] universal = 1 [aliases] test = pytest [tool:pytest] addopts = --cov=blurhash [metadata] long_description = file: README.md long_description_content_type = text/markdown [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/setup.py0000644000175100001660000000156315050330202014436 0ustar00runnerdockerfrom setuptools import setup test_deps = ['pytest', 'Pillow', 'numpy'] extras = { "test": test_deps } setup(name='blurhash', version='1.1.5', description='Pure-Python implementation of the blurhash algorithm.', packages=['blurhash'], install_requires=[], tests_require=test_deps, extras_require=extras, url='https://github.com/halcy/blurhash-python', author='Lorenz Diener', author_email='lorenzd+blurhashpypi@gmail.com', license='MIT', keywords='blurhash graphics web_development', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Multimedia :: Graphics', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', ]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1755426957.4465203 blurhash-1.1.5/tests/0000755000175100001660000000000015050330215014065 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/tests/blurhash_out.npy0000644000175100001660000006020015050330202017306 0ustar00runnerdockerNUMPYv{'descr': '};|9|9}:~=BGLRW\adghTTTTTUUTSRPNKHE~A}>}<|:}:}<~?CGMRW\`cegUUUUUUUTSRPNKHE~B}?}=}<}=~>ADIMRW[^adeWWWWVVUTSQOMJH~E~C~A}?~?~?ACFJNRVY]_abYYYXWVUTRPNKIG~E~C~B~B~BBDFILORUXZ\^_[[[ZXWUSQNLJ~H~F~E~D~DDDFGIKNPRTVXYZ[]]\[YWUROMJ~H}F}E}D~D~EFGIKLNPQSTUUVVW__^\ZWTQMJ~H|F|D|D|D}EFHJLNPQRSSSSSSSR``^\ZWSOK~H|E{C{B{B|C}DGJLOQRSTTTSRQPONa`_]YVQMH|D{Az?y?z?{A}DGKNQSUVVVTSQOM~L}Ka`_\YTOJ}E{Ay=x;x;y={?}CGLOSUWXXWUSQN~K|I{Ha`^[WSMG|By=w9w7w7x:z=}BGLPTWYZYXVTPM}J|GzF`_]ZVPJ~Dz>x8v4u2v3w6z;}@FLQUXZ[[YWTQM}J{GzE^^\XTNH|Ay:v4u/t-u/w2y8}?EKQUY[\\ZXUQM}J{GzE]\ZVQLE{=x6u/t*s(t*v/y5}<DJPUY[\\[YVRN~K|HzE[ZXTOI~Bz:w2t+s%s#t&v+y2|:BIOTX[\\[YVSOL|I{GYXVRMG}@y8v/t's"rt"v(y0}8@GNSWZ\\[YWTPM}J|IXWUQLE}>y6v-t%sstv%y-}6>FLRVY[[[YWTQO~L}KVVSPKE|=y5v-t%sstv$y,}4<DJPTWYZZYWURPN~LVUSOKE}>z6w.u&t tuw$z+}3;BHNRUXYYXWUSQO~NVUSPKF~?{8x1v*u$u!v!x%z+}29@FKPSUWWWVTSQPOVVTQMHB|;z4x.w)v%w%x'{,}28>DIMPSTUUTSRQPOXWVSOJE~?|9z3x.x+x*y+{.}28=BGJNPRSSSRQQPOZYXURNIC~>|9z4z0z/{/|0~37<@DHKMOPQQPPPOO\\ZXUQMHC~>|:|6{3|2}3~57;>BEHJLMNNNNNNN_^][XUQLHC~?};}8}6}6~68:=@BEGIJKLLLLL~Laa_^[XTPLGC?~<~:~9~89:<>@BDFGHIJJJ~K~Kccb`^[WTPKGC@=~;:9:;<>?ACDFGGH~I}I}Ieedb`]ZVRNJFC@=;:::;<=?@BCD~E~F}G}G|Ggfedb_\XUQMIEA><;:9:;<=>@A~C~D}E|E|F|Fhgfec`]ZVRNJFC?=;:99:;<=?~@~A}C}D|D|E{E././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/tests/cool_cat.jpg0000755000175100001660000011446315050330202016362 0ustar00runnerdockerJFIFICC_PROFILElcms0mntrRGB XYZ  ,acspAPPL-lcms desc8cprt@Nwtptchad,rXYZbXYZgXYZrTRC gTRC, bTRCL chrml$mluc enUSsRGB built-inmluc enUS2No copyright, use freelyXYZ -sf32 J*XYZ o8XYZ $XYZ bparaff Y [paraff Y [paraff Y [chrmT{L&f\C  !"$"$C" lMXxŘHܦp ̎YXP9FV,jFTMQ #&%\l`#Z7lx'W| yX;AP (` P s0 JL4RvM'n0 `tt~EMt7(-3xAf5EsIBDp8WƮ XGP\  +Z܋=ڞ̣ϝˋbc#= 9"C$ $3F2 Ap.e88Y#`(V4lp~gB,|Tw^D4"O3'I# PDf x`B3.U@bYa9+'*[~o4լg;k˯R-sx).mR@@p&.-y͜kK[@fH k!&KOOݡʮg_v:4\{mX^a)c哂1יѪ[^U웸eWW !F8 h#`Adڊɱֹhg,fԼ^!_--jzO4ާ%h3*29Kțyu$6꣑%69Mu5Hp\cJ<.Vh,T{) %IT!1jεs!vux..݊ RT ZpFJK4ZeZPĤط CoWwP<|/FXs&{{۶8 K pB3٪܋bxٞ.Ղf۸W*XV=Ԛ Snc,l۪4aIoTsWchd ]Ey2na4QR("G#^; UҎ.6fI4noYسbh{nu6qu%SF d@)o?mZcZNHI:$P/>rX/=M;u86#puAtZaޱ cE׸jl ~eY$ERjO^rji}Q ummdoJb[Yj7#6^NlEU'/H2ekA|zզa&J\R{7 5(=-ml綍54oV^Hrf)8ȟ$gkV.Q3.C^qrA^z #<]rpo#Pat #lJ:mV%֮ ӃJa *X6c'Bȗrrr>\226 ‚+igz=>Cԧt pܖ5 {nx6UpKNjjکUL~K^j?j^ڋSo5G4XyWqMdr:lq\+0ޓygSrAb |ҪZ^nѣCή%|޿;֫yOz 'e[0 T_JgS3'Pի5bNٱ*0U" LW]~./r)_սp~z=~ח,$ȍ\1͈! 9jVd\5r1Hff(I024,AȞ>HܽGywQuS8ˁϯ垹[Y7[9hBXt"IbG/c4^I} LkEF4Q$R0D:Y2906ȧT]}/VrR}+B9:3k<?V_^a.!1A2"B #$4%035;u PYywAt8u4|uV c-֣ w#)ƍ܍nCtS9ϊv .ѫlM7u2U>hl0vscQW J=ppt&r5l w. !x@mw?t6[gI]Hd8"4A;grdzP 5+[W/dxQXGNPþFcMd"@, 0S]FV6+yCeuƨb1]ZELyWHqac [׉'Y8tQ9N1^Xb-t祅 :4-+;rbtE 1ܠpY;W_?ҰEC¶:f(r!<&$ BaNM;~.Crܯ$p~ xtX(pL:kGҾi]gLEa8Yx_`0qGtzv8;S)X2m1[;pJRAQ4o4#F؄6Wt뷀@ݘM]8dS7[d{s.W{GK*z; 0D:Mhvq8GخF5r΃\DxJ:˰8t\S7ǥh Wjk=hHG8p@w'q 9AeneӄSwE? [,B3/ZdY+\TijmuT{oyY SSV[4T4?Ќ0]4g4nΘ](TmeQ).*P*+&Tu5Т8 9E (Slz*aOLWQn|QSN b6#7TAO+ᒒoSQ]xyNGS4K+vX%5'ßwIH׺*#Z}UTG?JIixglq 4Csp0_%?n5l=u| :FƬcv꾧]vxenP9]E,4Yᙃ$`nw`gcM-?M9#QK Ou.dAU7UK'&e1W#"hEg +OclcG 76@-nQkz,6v)ˢ5t+euo7W=jDv?L S =)V~EfZU?*{1R/]m@iqwN;.{+17]-/cZMC;;~7XLTs'n~Jnc;9*ӯ30؝}Q7xDb0DAl2w;0\ʺۅteGDahU-G뮴I'=wg/+ V;-Ӿ[/7_PwXpPCLG87bw/p_#K8_!h|mi @͋\_ƒ.'(l]J_Ca MܻKaS!(OH٨BO6 05op?%HWzkp {v7g ۊ9Czh+Tk0 xN\,+<,p9p8lWWbwO;|1;"?3MhplsvK,*K)d4cJ;q(6,Q~Bݾ=?ptv?;~~AlNc3Z:FĚOg`YW8Y(ݿ.!"1 02@AQ#3aq$b?i_7P>>g勌fwC鴙_AAwF~Gi ("rcC|+ҺTַ!<3WsКd TDE ߩ{0&p4ᱷwn4Pğ +ێY -wnSM .(~S՘۔c OP&4]Қ >}"e#p]i ,*Y y{1K(Ot'xRV)tyHMi-S +†pE=G]v !\y+DsêD'N@T N&oO >-P pmǻJZ-+NsԜ, '彧)Ed0X<~.M6=6p+4j`znM4(!05!DٿH+SjLD${<#d߅3gmֱ_wN꠩+'>AM"| hSw(cˎ8ȌBO8YBf{ v^]Gsn")p#S[ߐ CS/{r5Ņp} 6FY8֨Rh%;ti_!$ѫ9p|tZT6:اヸRdC+#.I1 {( ˩(cnVѲjKIPB{X[cK|)ٯ'tm[Fȏr;(XѰ[M11O>(ۡOA5O'QֱG3z}"A%`0IǍ>X9^6CI 9m8R[n@sMˇLɼDH5J|wy麮,xL[YC{AAN(]'&o֗ Sb,䕡#\NU&FsI5K?lg$QD fd;Nyw~V5˄&LayL{ +^ݩ rTR(lBFjw&4T;F0V\f+thf')MGuyҏt) *y"^luBqNii%N6gCևP9F }w"6Q$ RAs*\B.*`V7lB͸A)HuDҡ:ZSۥrwW~Lb7oq{}[F;!1Q "Aa2q#0BRbr34@Cc?V?:)0օcpp]]qN:+h8Օ+EkSn]}\Qqԙ ]֪N]Čd+xc~?gu%K0(W<;-x"op #uO?*6_p.&.' Z(u9\1<^7L\3d-`ѡe`!aZbF!wW]QPeGMu] (sO| (RLB c8R #l5)qn%8ʕq ] c1+,O\g}C$ )_AZb3ǧD3F !P0z ѩP(Ufk|aAQq8|%NAQ_)3ԦUCz ^>PUB€eNh5Gfy\lغ,wx$x`#.2 G*p.aes;^+{N@WU4wwf1A(ZӆE (mg`SR,L7^:9Bczp62uC3-9EZ\gc`/ܽg;)ouWo.׏7 ykK#T> (=S 6LNɞ:sR(ǾGD82Y[y_Du!* e-?xݛMʫuXuBimEW05Yw =7e)G qGHVfLxŌ@L!_O/E+T{*YRD7VPvd}Nq^sKpV<@ *_QԋF0q #a=W1M-I'ZpQZl敐~djoQBEpێZp8!-nkl`\IC @Tx]r8el.W0.^OÔr2&U[8Y#'ӊ,_ѫ9{r5qYRcZ;q%Yx ~$ZVg i%H]u0 7cu*i>W'=Ge^㑻\q^VJL *P4~)vŨi7(ɘp 089ފchr^?}T*Ou-_PC6uԺA=[bF8T`†>{FPK7?e.7n%6z㒤ƶ^a$eYr ܯF{mxCZ$ WVT=?1JlDDT0 ?WEEM0ή+/| %5 9f #yN=0RZ'{Ʒ*}nڪ>CV4NXh4,}۹*SNW^ HEpl3wBV[c#NMW _^'9?e`SjN%54[Bx I߅XU_ׂDSw݄쮡P胈-#Buwt Ȩ&.Ln˨,Ym~'KVJ*VPlJ1M?4g*^=1e=շ  JcGGN$|5D)jHI=]X(P5ú-Ni,ql)jee:QocQ#g^+tʮYAٓ)p¶6qTPx]U'XEr ӂ0Yၾܣhk%N7 \nG1q6FOʎܨoDOPi/fa \҃d# ӺgdX=G9d\jO+-00׀O雇X'gk]E3cTae::G쥦B%ßdtވdil= 9bW/Vh.nOSk~7UTգ)Ovc״HpAt]N*:`:kbVSN6tXY:SHݚ?^yPuFꥲl8"[GN Oݰy VlgBF6+Q{ pPD *N9ƴˆqK|AL?jk7pu3a0bEYSϦ}T ک:%Ø&T :'3ҘJL{N•] سG\!]hx%@6;!Că^0G-°Jb*0 OSݼ^S[+ye%aI",q(wBnV7^8~ F%V•N }1Q~nGʶbHզVBl+8)?vӤVs YVa8,J1 М w.QcwRq Vw*G 8Su9ը[=ZP#UiJTswj&WP]뇮7Ƨe7g)PO1йV=l4ZN4ClȳBt*q ҠSL*FoSgKd}^RWǶ)_*|*?C;=V=nq' 9SS ᛣħNaV:39S_U،`询ʲ7S~G¥Y 2rf8w_Պ~yBg8@rN>|k TuU0FB:U S?b <VRlɈ\hzgLef&!1AQaq?!<1Lf"np.nXvMarգg9JV<@u\Pxx*;wZ #uĵ ˙Ia|EE7CI+)2GQ! ?Tʀ?Ies> _9@dr#Dƈ+D$M LF`GJ*W:bLT¸ڹˆW'@MErM2Uj RK 0̄N%rXNb|ah( 蔮c|ػM|PKɿm]\AbfY{@2'=6xa%^6Ia[FF -A9㜻PR{"N4:DJZdLfX+r\Ƶ3=ÆcQUwroV( 7W;"w9\Xәä(|WLBdJ&q}f5%˨*# nCxeJP;X;E#M}L" ^¸3N~Yvd^&EtpErXFi:AT5TBlmzr1[09nU0Sc8( {iCZe <1xDqbnik~rLe+GA4gS1-tbi4"pD% 3iWҧj$9aW|m-14a}P*g;\^y,bPXOxj+a¼ NCr٪#J:gC /%_3%֜N*o?L2)}I/ ځ8i%fcYaF/V Uiw&s86w)c`mӠix`md̮L6bRWrkxSGFOԏ19.1-VT{q-ԠNZx&Q߲[Xn98+2vL%^LT|vcf8 f8Ks..R,gŭ̳x+ј0yf0s EqA4="_"1TῨ;SщWΔs7osP1107}Ԩ,ɺ o0dr#-?1RЉD)#o7D;\UJZ(37R*`6f<8kP>%SS__ ~Tle3A,}_iQQENb'H"%桺sQR\Bqt4b7^(Ṛ=Rz.X$.m-]L! [20tB[x@UAɸNg-Xh~p0|D5rU`撰O3`]TU)gl5,R(]n3MJp M Va7 A =J95S\eBYxW4k* G</s΍l'AA9ή=23+UhE!f0iK&eB9~-A qrU0K/{-/bn!s!o p 1Kf=_5 L?h8S/_=qxyW(ZqRɉ7Mj|7qj]0x{ @ !Zd]?8-i9iȆ0祖*1=d^36@r?GZ Qё2%,J_wpap|Fy,9h !FC ^^fVg*NĮ0Cc0RLCN+S$fZX#pwԷL5йnVVbնpyy0mT8rc KůpD|`z]H8)!sokԢQ<QAG+.ᄂ \0婦LbcZoL+1P>eWWQFck u߉"\P*0ۃW  Sk(1`AMipĺfS5o,fl)y<-FAr6g?d2ԣܽ*0yfG/IeU tXƳS1ƌQ,ۯB.*2M4K56B 7-Dh̷fPyLxQy ZTja3)=%_:{` 'J/3s\22,PVRw*6V*ǩ CY^JICE㒠33Sd ņ pJTXe'Iy>%/ajlo+NwsL(ڴJ 6pUG9>#jHTSWa]1iK*QQp0,Y$'Ё(UDl4mJ%"n/>:ltL/ w(ۀS) BYqL+Țgb9aݣJ7U3%Q^al`1k̤pANc2>`;ԭT,D Fhq,/:+%_ZXIf/$\KnNjIsou< ?6 /A}Snx/q֘kG# z w77# ǘ9dVE'ɔJ.D#f & M_p 56ka<~2!p`t -6 55Bs:B>P PFRVF7AP& hTKgx j328LCZ%Wq*f[[{dˌ̮Ӭ |B.0Φ4 zP9Lr3DwP^ݞ%6d/QVcDP?3>IZXOVS99u٘acBKns~&F_YY+QcX'u&wKٝL1sZt7wq5q6o+px)-xvN?91Db W 1nQ *< j3rޥ->j}+1N^isS-zXZXiGD0_5s\aGuF<g)X8a.o:pz~ )qJ9^ ۨ,x}cluS(L1˳İ3lvj<5s>ehFv̻0y 8 /2ZeAnxK1ceq,"Ru*xq:,2th+r?gG,*\(㝚YEB_DY;UG4eHTv:sƎ*Y0_~߿+4ǹk*=(7 6'н*2u̠\bi7 \lĆPV8LA+7f8OٛmVՖLDa1 >i_uc0@+<|5{ay23-8<H&. ep3 ݉j(ǨCBW8Yh_k jCQ{7.Ʒrꯨ:t:wY~,o3D<'w̾lr@fx cPHΒsN n5]ÊIS8hø3ұ0Wb1̱r.z`7ֺ NVſ@J|M1L5'LJn`1-X!n;OQӊb5{LY-fU0Ls< q.R0^3X1 K'6x){MTU.+w,`<6`r@d i//ܩ`bLBDԻ/wT ?}䧶*L-,7SF])9gN>>p:KNCL]ҿq >Cs~J\LL YjcWZu5BR07ܠJ2^.'<ѹr  ş#Vtс tQx`Gm?^ _ La %n" ib{86-CBfnqr4z7/Į[AJd)Gn͇(|C-` ,fN@(w6xQsdw0w\^ 9 `!yv=m;Z7r)YUhFյ1p\,o$( @d3- vzi=;C:J } +~bHQ\ 4H*V& u)| 8)RR1ڷg9He?-]<πdAY;FP1A86#Pk@lr4b8|~"]5|8< , M~SiDp~I&Ǩk1) b*&nPN 8@W!?p.JPUbpjc0;&2ݫ{jkp+Pcw<8RuqA4~ FuiM1:M-_L N_]K%c%fp@4aJjI |GӐ?B[x~c,s Md@_̯N]b3RnmQlU\f7Ǹ̫[!@B\f礱.8oÉRhSWLvm 2,3*2^9RÇ$uNf/0Ifd׉Ă~"bDL02C\Rwye3,3ӊ'Rŋ2LB WhÚ0K -ű|Fx~eO!FQpp 7Wihܠ1 Ѵg&aAI-ref~"Uͪaj3e 8_7쀭qg}g>fPؖÙb0'W ̳XrEYrIaL++LWTC~P$ 3_*sq(GE?HU0M-m&%5%ޥoO$ YW1.O-f]>sY$sJc-Bن^[SV&ܽDUnR/lAdY@1F"MacƵģ!_`8R?Ƨb88S$rQTdV+ψ/mBZ&~cb-?S&w]B*AR9[z^nkڝ %2IJd!ō} :8<w~ZCjblo`/sGcSR|ð" &vڤHAoSC8F$ds,aY6~Ns.?^G!Y}꡵5 0صrJj>;(@U%M^2'?~pDf?/Je,[}!prw3.6ef㇏DJ/eU|CJ2}C,XKEUĻwK @Gqaqй{\e83C,h5,=k꽠t#f)fP1M.TCh%`1,H:oYJ]#mm?(!1AQ aq0?һ  /c*W@tHt;9ƄЈץ~a\WqGJ E?v+tz5 O+(N>: QZw7RW֜  cFW*20e Zx>0k<}?pOR3^ ruyCO aA\uYq Ak~31J:@–e\~克n0 =W@0źyq,.|Zx2De֕ɬ%u*l=h%ӷ CQZFE. *hNL]E d7fKBߜ ;- &ZcOxc$4H . WJq샚DW|;XWqo*6͹5 OnZA2V~{,Wx P/}{WsmTEm ʓAĒ󜅎-olv<BqNǏydvQ v91YXJ^bsÂd6/0:LHoSzph~Ymo8<ؖy0b eK"cR XĚ73$ى0 wVvsAx˄J4 ufx@h=7 Gu, (Q w2^B> B٧$N8R%m5-RĊG[o(h|9M GZ>ViU'/"a 2O`!zg~EqAy_@ē9B=9qFαDЛs6unk`t.:k ;/-N3RO#5mmm!&!Ö 5"\%@#{1+ZNjAI҃8Q0TWItH{;~xghoxmNt:2q9<"-_/qX9Mvvo@{5V5{ \B@w {޼[>xaxiFȣX 5j41@=4q)L( 9 %a ķ{lxz*Fƀ@ycaɨl̍x Xu5ˮm$9E%Ș>f]\#XD(gȜH4\~rr2=♺%b5'YR5_9)}SބţAva \yKvx$jmn8mCK-,h@0öJy#(aa^=02vb\0}'M^0 :R*7!R2ϑȒ6༖ƛ^q9 Bk7i7kcgw]|m6r|)iFD$4`X N$&ziY jy1BI(p7vM:X CP]g'Wܕm w<=cqx8Ph֗E;KKpffN9i&Ab'^r|ilL:C}d+w(ΰE9:a6na pšQLܓϜ`t"L "% *H‡O('AFxz)Ǥ) 8;b\uQokh^ `j=r]9%n}` s0W%v'<9ҧ%)+L1J '+ZE?ti{\(iy1-q:jۈBPؾrXlu/{Md&:ݬ4G8( X_YRj- D65'm.G,!.J_|)7' Z@(Jȣ i`L<܊q1F2:Py:q5N "o2wPffN*pC!2Ӭأӝ1nqk"щz=zi&s"7R#fMq3?РP|b3bJU×N MN1F s<܀A=`E8ZF<7U }ANC8Ba Un= gno P}o\f )tR 1o~0 &gnNpۃGxȾUr1R Lv4ֱ?M,B25x cfVs\c<9D1CȞx%0QP;qh|\fz5}з81h0V!vZ$GQI1^HW: `8j]tr}~p뾄6ކ>h/{+ESK"J!qy׷#-5ާ$ᡠd;7t pCxGuCf!^3]dpK+tą F v$F;2Mk D%5!*N{p**"vkH6.D;fnYbp%Ya㔤]$*&%k,DGTǥ5h~WtۛеdS(M YfP+i&6H(!uqp(%;|?l9hN BCXDLU4#ihy.0+j)K|z<+ A? l nusw(oWA4qPd u2S@)/M8FozDO#$a+ .R?CP8<|{ȴM*߆JZ,- `YQbvrhE.{ x+kQ*8M2~p*4b$,o*E˗w[ ᏐLkUnnxg#&WoXNd*(9Dh )@0ufgcOX&D,#`a*<]/JdNӡgQ鈠F1ARu7iyˁlN)>_jM$Fɣ|I' ߊv㠒!DB 0[FMW7Z޸͵ pusN$.k0 K-g%v4!>q,qt82P{!WwGB9í7c!yCFfwlh491:E><5Pij%|q@Vό3`_n|kaгybj8D'WAN+qkAo)7 R=>p#u~#^]cR*GA_~@2qi4f m3Yqq>0TA"::3i#+JLGI& rȾɎ*Ao&@ @iHSΟ=#\@ik3xi0`at:Ҫt\OZ7qA'Yܻ^\Vkz]e$*Nc@OqP:1 Z2b*4C"hk2'E T{samG#&1Deh ӣ&hxӜzL0QC|5 p"NGc7rȦwwz zGdC#銁I@N>1cp1qO ~,VinSnrl&bRmO@]LGayK ~I Knapc d`>[wwBw;1?tʎ|1Vy8nn`"=:T 9)?F5iDd4k#3kGKE:&TeE`'BkɓƘ+^O8I8K`A'8D9 96I6׫D*W8<9 !C@&}`Mx*֦) zʄ57Qn Y*xv> Hǃ$8<a?m9&ѹ 3rCz5wePw4d$oPO'BqhCL8L dPʠ8H`11а{ptC(88V4bUrAHxgn2,IdGM뙎7e 6K_N 4` 9H+j#M8U/o}8 '8IO{˸q;Wˌs) 0鏪a2&},}evXUaɊwt@@ )bN3dnw \C 휮ASsDŽT lv`)$h>C.Jg=HYł ʘ?;XNxĐp9?o9f"n_ĶyR[S83֮mdFxtNvfkX@u&+֛ū@4`1x% Cclvk&BiMdOI~u;|y5Z/bL^>isSyŽSfOSacރk/0xҽ83@,|gA a!tvi^E1(u'qa|Ep sc@;x 2A7Uu`oUl  sUg5yl(oߌ rwFGѥQw6&5V{C X @׋u8QbyqD/e`ਇ1Cme0oX&#ѝ"1$AQ<9z@5E:G,#̲֜8F Hw[ 5 CCkeqlkPsAO=kF S|NJ&iOH/A 5qk|SD0+-x~( 7kX)C5 GTK܇ N rlAGq9%wkoxfThˁ:(Pt&mнc:yۯy' H9c7ېO/az{G%n#}[p/ELyxL8#Fɛ^'ϼ(3Mݶl:,e _nuD}8ݵbW;Gh~0X +͑DRA397]#Am@}Kwg0%{ qo⊂'WlKn33y 蝚ˮ0Tγ)?$Ab@OW-ؖw7CX:& Xjʕlu d,Qt|aP>pTY:pDO1 Q(=$bg`|/&!i9Ćq58&N(n՝^7v8$FU ji!j?,2MZyV~ .W brH) d6oLJ'a;z>!q'kz#.PlCc$/Ky!ɏ}r~βTZ HP|;)9Ni"sddc?vM rpbw|#,N6X{=`sq8TvwThA>0l?XX:3fCF_p 2 ɟif ypʹCubLQA5PQuX\|H1혈TJc*{矼1gN6|ejD9o= ߬u&WGП`ThNX#Tߡ"D7p5xp80 ^p%nौ©~N=C/东VMD _@`S 'Ka}ηЛtł/nnx1CÌaNѳ)ӁЛ(p`.[LpFLV1ږ!! ncC]h8Ǥ p۩8v%rP^_{qN'ɌKL 4ق#_fZ~raP{ Nq,6o`Lr 5w -&SihD/S:٭>H7[U~Ye\=ZaW}c޻ Ni>8X$0[zHDj˒v+.Іba@m.)jQPeHҥ6pBjUt4^]3_DbJ|2! эIq SNSTW#BW#jXxz/Ķ"Qvxȴ :iQva z%.\wSHuPkV8ʀx GÉ3qHTtq8\9:"7~L Ƴ[ɡ]3ȫ$ .'6hcٿ&l&_We]b9(ͦ+@Nk94nD.UlYFX(GE0Ɉv?QClܵO,^LFo1MBR3SD$"`i`z0Zv_9O$5x?͕VsNpPgI oI! uITm @ Jz*_q-ypۛL)*j̐C;].b!Ad-<-Hy9{^ e ϒ\Q*A<``y~*oLbm#{06s6 B) >#nƱM#C:pDA"y>f*KwCrD%gc+\=]q#T7ltl}g68;~|d|o~jAɽ^(=ar? +eq'fDBk6!ӈq|L+tx yT=;!s\vzDxT0+ME_ ZT0/|7-8UcV*Pc҇+Ȓ/)WnSxL6;փ t.i;~0y펨yڙ.㚚_^y' x*qv'(:!H]vi߮Kغ9z kYH;+6W3Csz[Q?;LDNn~1 p7-%8orxCN5_O9DZ/\8<D598 I>ɛE[䋚,rYdyV3؞qH\ (9gYV%țqB_V*INwA)`IHJp8h9eBkXQPgPCjWé7_NCk)ܮ qXnC&89Y1̉ h\_ywяLu0qE/!}eAM8(GGb0 J'..<Hۂ=5!4~Yk"r7kw&oLpT=em". b ldʁOew ˚G$W˜7}TT;UяVf e1L:Ż >dz-Sx].o-Γw4%M7Hoȑ &)]KbgY1mE.h͘rHWNPutl4"@h}=`j.@{SiƁ=߬X[d!`8[:hN9?:> ?*(NL:8odAu&+{#vu*V.3&+Yn%q!sqq/%sx$2ሿ[ ]6R0Agye9{2au"()" _-W_g7-N]P9f1]TwsUyMwĪ0% ޼`*Zto;0"R*1p!b. wnLaxyNpaBcx;l@F C8ƒjR$pRlG7JE\*̷, \V5jt4nQ֋q&I*^PI~x=D ,`~1WJdfckOLH|mY( (.C~ ^ |xG ( iғ差A]uwd@Fa(@jDKʑBJ hx~pfYksWYZcσ kx"c8B@ <.ٵZQV@1wʘ@jɨ% h]/{*"zV.pXi{(lWKlzhs35O4ӱ6yxnj4U)^_8fjvCA1haBsk/\Wj#QO>r?slҧӔ[wE)"$'c9p%لN$V;7RoxˇKӂ(b'xh/˒?*_B EAR8za u."rMZoȔ鿨ҧ )k|c) h.|x4`&Cw5Yh=f/cPrO8K(ѱ60pCM<6/,W)L^t91:mL1J^1NmF$=w8'C)%/gX)eaQr_QX;βEG-(rQ VGO#v~S0RW֡ قIzDZGwxk!HuV׼ҘSZ:v`#HhpLJWI?xf>02$:=d`_7oM8D ĘmXR_]8+L\܄ ษd[w=`"o/(ZHpLza}[ ΓTOHUf؞8lHT~ Sø擌W(K7pхct K!~%8d#)p :^qլSMpJ<1]LfH &[Tn.3ӒW5(.jfrf̅z!`2tO976/]tML g(.J k ~oWjat>0*F?,zf.Z+eCf1@$&&8;Ն?9ՠmf]@rirwȿX٣~qY+Wȃ}c@^0 )lx¸Xi}7N^GXT@9KǪi0 .qENR>2ژ͵[]剡;Z^SsP 1朸wx."xv8?2]:p ; tqʶd˒iX7*N@+ 'D-+-~ _dOFb1 @< gQaW?X=z1+h{aF{p8y˴.ZQyɩyt99"`_.Gqr xȺIr Hi?e =)C ʏ1a-~2'xL_G)D`0(/H~:OG_xqGV2m#Ti3bGO'2^ ˡ!%1+9)yM'xA@phkoF""xY=hѪֿW#`NWACtyUOf@;#btCn #SGq~(>'H:u~L>dUwq-e}elڜI8@Oe۰N˵QGZ+h#C(%>rca6|dRˉQP{pJRRLG'q i o}epsƞs] ر;҈I ,GJ!;]Ta [jx]n g& p5Z*'wKh6././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1755426946.0 blurhash-1.1.5/tests/test_blurhash.py0000644000175100001660000000544515050330202017312 0ustar00runnerdockerimport sys, os base_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(base_path, '..')) import PIL import PIL.Image import blurhash import numpy as np import pytest def test_encode(): image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) blur_hash = blurhash.encode(np.array(image.convert("RGB"))) assert blur_hash == "UBMOZfK1GG%LBBNG,;Rj2skq=eE1s9n4S5Na" def test_decode(): image = blurhash.decode("UBMOZfK1GG%LBBNG,;Rj2skq=eE1s9n4S5Na", 32, 32) reference_image = np.load(os.path.join(base_path, "blurhash_out.npy")) assert np.sum(np.abs(image - reference_image)) < 1.0 def test_asymmetric(): image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) blur_hash = blurhash.encode(np.array(image.convert("RGB")), components_x = 2, components_y = 8) assert blur_hash == "%BMOZfK1BBNG2skqs9n4?HvgJ.Nav}J-$%sm" decoded_image = blurhash.decode(blur_hash, 32, 32) assert np.sum(np.var(decoded_image, axis = 0)) > np.sum(np.var(decoded_image, axis = 1)) blur_hash = blurhash.encode(np.array(image.convert("RGB")), components_x = 8, components_y = 2) decoded_image = blurhash.decode(blur_hash, 32, 32) assert np.sum(np.var(decoded_image, axis = 0)) < np.sum(np.var(decoded_image, axis = 1)) def test_components(): image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) blur_hash = blurhash.encode(np.array(image.convert("RGB")), components_x = 8, components_y = 3) size_x, size_y = blurhash.components(blur_hash) assert size_x == 8 assert size_y == 3 def test_linear_dc_only(): image = PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")) linearish_image = np.array(image.convert("RGB")) / 255.0 blur_hash = blurhash.encode(linearish_image, components_x = 1, components_y = 1, linear = True) avg_color = blurhash.decode(blur_hash, 1, 1, linear = True) reference_avg_color = np.mean(linearish_image.reshape(linearish_image.shape[0] * linearish_image.shape[1], -1), 0) assert np.sum(np.abs(avg_color - reference_avg_color)) < 0.01 def test_invalid_parameters(): image = np.array(PIL.Image.open(os.path.join(base_path, "cool_cat.jpg")).convert("RGB")) with pytest.raises(ValueError): blurhash.decode("UBMO", 32, 32) with pytest.raises(ValueError): blurhash.decode("UBMOZfK1GG%LBBNG", 32, 32) with pytest.raises(ValueError): blurhash.encode(image, components_x = 0, components_y = 1) with pytest.raises(ValueError): blurhash.encode(image, components_x = 1, components_y = 0) with pytest.raises(ValueError): blurhash.encode(image, components_x = 1, components_y = 10) with pytest.raises(ValueError): blurhash.encode(image, components_x = 10, components_y = 1)