Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 89aa9a6

Browse files
committedApr 4, 2023
Added CLI + updated README
1 parent e1bec6b commit 89aa9a6

File tree

7 files changed

+248
-14
lines changed

7 files changed

+248
-14
lines changed
 

‎.github/CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ $ source ./env/bin/activate
1818
$ .\env\Scripts\Activate.ps1
1919
```
2020

21+
Make sure you use the latest pip version by upgrading it to prevent any error on the next steps:
22+
```console
23+
$ python -m pip install --upgrade pip
24+
```
25+
2126
Then install the project in editable mode and the dependencies with:
2227
```console
2328
$ pip install -e '.[dev,test]'

‎README.md

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,154 @@
1-
# thumbhash-python
2-
A Python implementation of the Thumbhash image placeholder generation algorithm.
1+
<h1 align="center">
2+
<br>
3+
ThumbHash for Python
4+
</h1>
5+
<p align="center">
6+
<p align="center">Open-source, end-to-end encrypted tool to manage secrets and configs across your team, devices, and infrastructure.</p>
7+
</p>
8+
9+
10+
<p align="center">
11+
<a href="https://github.com/Astropilot/thumbhash-python/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
12+
<img src="https://github.com/Astropilot/thumbhash-python/workflows/Test/badge.svg?event=push&branch=main" alt="Test">
13+
</a>
14+
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/Astropilot/thumbhash-python" target="_blank">
15+
<img src="https://coverage-badge.samuelcolvin.workers.dev/Astropilot/thumbhash-python.svg" alt="Coverage">
16+
</a>
17+
<a href="https://pypi.org/project/thumbhash-python" target="_blank">
18+
<img src="https://img.shields.io/pypi/v/thumbhash-python?color=%2334D058&label=pypi%20package" alt="Package version">
19+
</a>
20+
<a href="https://pypi.org/project/thumbhash-python" target="_blank">
21+
<img src="https://img.shields.io/pypi/pyversions/thumbhash-python.svg?color=%2334D058" alt="Supported Python versions">
22+
</a>
23+
<a href="https://github.com/Astropilot/thumbhash-python/blob/master/LICENSE">
24+
<img src="https://img.shields.io/github/license/Astropilot/thumbhash-python" alt="MIT License">
25+
</a>
26+
</p>
27+
28+
# Introduction
29+
30+
The thumbhash library implements the [Thumbhash](https://evanw.github.io/thumbhash/) image placeholder generation algorithm invented by [Evan Wallace](https://madebyevan.com/) in Python.
31+
32+
A full explanation and interactive example of the algorithm can be found at https://github.com/evanw/thumbhash
33+
34+
# Installation
35+
36+
You need Python 3.7+.
37+
38+
```console
39+
$ pip install thumbhash-python
40+
```
41+
42+
# Usage
43+
44+
Create thumbhash from image file:
45+
```py
46+
from thumbhash import image_to_thumbhash
47+
48+
with open('image.jpg', 'rb') as image_file:
49+
hash = image_to_thumbhash(image_file)
50+
```
51+
52+
You can also pass file name as parameter to the function:
53+
```py
54+
from thumbhash import image_to_thumbhash
55+
56+
hash = image_to_thumbhash('image.jpg')
57+
```
58+
These functions use the Pillow library to read the image.
59+
60+
If you want to directly convert a rgba array to a thumbhash, you can use the low-level function:
61+
```py
62+
from thumbhash.encode import rgba_to_thumbhash
63+
64+
rgba_to_thumbhash(w: int, h: int, rgba: Sequence[int]) -> bytes
65+
```
66+
67+
To decode a thumbhash into an image:
68+
```py
69+
from thumbhash import thumbhash_to_image
70+
71+
image = thumbhash_to_image("[THUMBHASH]", base_size=128)
72+
73+
image.show()
74+
75+
image.save('path/to/file.png')
76+
```
77+
78+
Alternatively you can use the following function to deal directly with the pixels array (without relying on Pillow):
79+
```py
80+
from thumbhash.decode import thumbhash_to_rgba
81+
82+
def thumbhash_to_rgba(
83+
hash: bytes, base_size: int = 32, saturation_boost: float = 1.25
84+
) -> Tuple[int, int, List[int]]
85+
```
86+
87+
## CLI
88+
89+
You can also use the CLI mode to encode or decode directly via your shell.
90+
91+
**Usage**:
92+
93+
```console
94+
$ thumbhash [OPTIONS] COMMAND [ARGS]...
95+
```
96+
97+
**Options**:
98+
99+
* `--install-completion`: Install completion for the current shell.
100+
* `--show-completion`: Show completion for the current shell, to copy it or customize the installation.
101+
* `--help`: Show this message and exit.
102+
103+
**Commands**:
104+
105+
* `decode`: Save thumbnail image from thumbhash
106+
* `encode`: Get thumbhash from image
107+
108+
### `thumbhash decode`
109+
110+
Save thumbnail image from thumbhash
111+
112+
**Usage**:
113+
114+
```console
115+
$ thumbhash decode [OPTIONS] IMAGE_PATH HASH
116+
```
117+
118+
**Arguments**:
119+
120+
* `IMAGE_PATH`: The path where the image created from the hash will be saved [required]
121+
* `HASH`: The base64-encoded thumbhash [required]
122+
123+
**Options**:
124+
125+
* `-s, --size INTEGER RANGE`: The base size of the output image [default: 32; x>=1]
126+
* `--saturation FLOAT`: The saturation boost factor to use [default: 1.25]
127+
* `--help`: Show this message and exit.
128+
129+
### `thumbhash encode`
130+
131+
Get thumbhash from image
132+
133+
**Usage**:
134+
135+
```console
136+
$ thumbhash encode [OPTIONS] IMAGE_PATH
137+
```
138+
139+
**Arguments**:
140+
141+
* `IMAGE_PATH`: The path of the image to convert [required]
142+
143+
**Options**:
144+
145+
* `--help`: Show this message and exit.
146+
147+
148+
## Contributing
149+
150+
See [Contributing documentation](./.github/CONTRIBUTING.md)
151+
152+
## License
153+
154+
`thumbhash-python` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.

‎pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["hatchling"]
33
build-backend = "hatchling.build"
44

55
[project]
6-
name = "thumbhash"
6+
name = "thumbhash-python"
77
description = 'A Python implementation of the Thumbhash image placeholder generation algorithm.'
88
readme = "README.md"
99
requires-python = ">=3.7"
@@ -56,13 +56,17 @@ dev = [
5656
"types-Pillow >=9.0.0"
5757
]
5858

59+
[project.scripts]
60+
thumbhash = "thumbhash.cli:main"
61+
5962
[tool.hatch.version]
6063
path = "thumbhash/__version__.py"
6164

6265
[tool.hatch.build.targets.sdist]
6366
exclude = [
6467
"/.github",
6568
"/.vscode",
69+
"/scripts"
6670
]
6771

6872
[tool.isort]
@@ -107,3 +111,8 @@ ignore = [
107111

108112
[tool.ruff.isort]
109113
known-third-party = ["thumbhash"]
114+
115+
[tool.pyright]
116+
reportUnknownMemberType=false
117+
reportUnknownVariableType=false
118+
reportUnknownArgumentType=false

‎tests/test_cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from thumbhash.cli import app
2+
from typer.testing import CliRunner
3+
4+
from tests.data import ENCODE_DATA_TEST
5+
6+
runner = CliRunner()
7+
8+
9+
def test_encode() -> None:
10+
for IMAGE_PATH, THUMBHASH in ENCODE_DATA_TEST.items():
11+
result = runner.invoke(app, ["encode", str(IMAGE_PATH)])
12+
13+
assert result.exit_code == 0
14+
assert f"Thumbhash (base64): {THUMBHASH}" in result.stdout

‎thumbhash/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cli import main
2+
3+
main()

‎thumbhash/cli.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import typer
5+
from rich import print
6+
from thumbhash import image_to_thumbhash, thumbhash_to_image
7+
8+
app = typer.Typer()
9+
10+
11+
@app.command()
12+
def encode(
13+
image_path: Path = typer.Argument(
14+
...,
15+
help="The path of the image to convert",
16+
exists=True,
17+
file_okay=True,
18+
dir_okay=False,
19+
readable=True,
20+
resolve_path=True,
21+
)
22+
) -> None:
23+
"""
24+
Get thumbhash from image
25+
"""
26+
hash = image_to_thumbhash(image_path)
27+
28+
print(f"Thumbhash (base64): [green]{hash}[/green]")
29+
30+
31+
@app.command()
32+
def decode(
33+
image_path: Path = typer.Argument(
34+
...,
35+
help="The path where the image created from the hash will be saved",
36+
file_okay=True,
37+
dir_okay=False,
38+
resolve_path=True,
39+
),
40+
hash: str = typer.Argument(..., help="The base64-encoded thumbhash"),
41+
size: int = typer.Option(
42+
32, "--size", "-s", help="The base size of the output image", min=1
43+
),
44+
saturation: float = typer.Option(1.25, help="The saturation boost factor to use"),
45+
) -> None:
46+
"""
47+
Save thumbnail image from thumbhash
48+
"""
49+
image = thumbhash_to_image(hash, size, saturation)
50+
51+
image.save(image_path)
52+
53+
54+
def main() -> Any:
55+
return app()

‎thumbhash/encode.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,18 @@ def image_to_thumbhash(
1313
image: Union[str, bytes, Path, BinaryIO],
1414
) -> str:
1515
m_image = exif_transpose(Image.open(image)).convert("RGBA")
16-
width, height = m_image.size
17-
18-
scale = 100 / max(width, height)
1916

20-
image_resized = m_image.resize(size=(round(width * scale), round(height * scale)))
21-
m_image.close()
17+
m_image.thumbnail((100, 100))
2218

23-
red_band = image_resized.getdata(band=0)
24-
green_band = image_resized.getdata(band=1)
25-
blue_band = image_resized.getdata(band=2)
26-
alpha_band = image_resized.getdata(band=3)
19+
red_band = m_image.getdata(band=0)
20+
green_band = m_image.getdata(band=1)
21+
blue_band = m_image.getdata(band=2)
22+
alpha_band = m_image.getdata(band=3)
2723
rgb_data = list(
2824
chain.from_iterable(zip(red_band, green_band, blue_band, alpha_band))
2925
)
30-
width, height = image_resized.size
31-
image_resized.close()
26+
width, height = m_image.size
27+
m_image.close()
3228

3329
hash = rgba_to_thumbhash(width, height, rgb_data)
3430

0 commit comments

Comments
 (0)
Please sign in to comment.