Skip to content

Commit

Permalink
Added CLI + updated README
Browse files Browse the repository at this point in the history
  • Loading branch information
Astropilot committed Apr 4, 2023
1 parent e1bec6b commit 89aa9a6
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ $ source ./env/bin/activate
$ .\env\Scripts\Activate.ps1
```

Make sure you use the latest pip version by upgrading it to prevent any error on the next steps:
```console
$ python -m pip install --upgrade pip
```

Then install the project in editable mode and the dependencies with:
```console
$ pip install -e '.[dev,test]'
Expand Down
156 changes: 154 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,154 @@
# thumbhash-python
A Python implementation of the Thumbhash image placeholder generation algorithm.
<h1 align="center">
<br>
ThumbHash for Python
</h1>
<p align="center">
<p align="center">Open-source, end-to-end encrypted tool to manage secrets and configs across your team, devices, and infrastructure.</p>
</p>


<p align="center">
<a href="https://github.com/Astropilot/thumbhash-python/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
<img src="https://github.com/Astropilot/thumbhash-python/workflows/Test/badge.svg?event=push&branch=main" alt="Test">
</a>
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/Astropilot/thumbhash-python" target="_blank">
<img src="https://coverage-badge.samuelcolvin.workers.dev/Astropilot/thumbhash-python.svg" alt="Coverage">
</a>
<a href="https://pypi.org/project/thumbhash-python" target="_blank">
<img src="https://img.shields.io/pypi/v/thumbhash-python?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
<a href="https://pypi.org/project/thumbhash-python" target="_blank">
<img src="https://img.shields.io/pypi/pyversions/thumbhash-python.svg?color=%2334D058" alt="Supported Python versions">
</a>
<a href="https://github.com/Astropilot/thumbhash-python/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/Astropilot/thumbhash-python" alt="MIT License">
</a>
</p>

# Introduction

The thumbhash library implements the [Thumbhash](https://evanw.github.io/thumbhash/) image placeholder generation algorithm invented by [Evan Wallace](https://madebyevan.com/) in Python.

A full explanation and interactive example of the algorithm can be found at https://github.com/evanw/thumbhash

# Installation

You need Python 3.7+.

```console
$ pip install thumbhash-python
```

# Usage

Create thumbhash from image file:
```py
from thumbhash import image_to_thumbhash

with open('image.jpg', 'rb') as image_file:
hash = image_to_thumbhash(image_file)
```

You can also pass file name as parameter to the function:
```py
from thumbhash import image_to_thumbhash

hash = image_to_thumbhash('image.jpg')
```
These functions use the Pillow library to read the image.

If you want to directly convert a rgba array to a thumbhash, you can use the low-level function:
```py
from thumbhash.encode import rgba_to_thumbhash

rgba_to_thumbhash(w: int, h: int, rgba: Sequence[int]) -> bytes
```

To decode a thumbhash into an image:
```py
from thumbhash import thumbhash_to_image

image = thumbhash_to_image("[THUMBHASH]", base_size=128)

image.show()

image.save('path/to/file.png')
```

Alternatively you can use the following function to deal directly with the pixels array (without relying on Pillow):
```py
from thumbhash.decode import thumbhash_to_rgba

def thumbhash_to_rgba(
hash: bytes, base_size: int = 32, saturation_boost: float = 1.25
) -> Tuple[int, int, List[int]]
```

## CLI

You can also use the CLI mode to encode or decode directly via your shell.

**Usage**:

```console
$ thumbhash [OPTIONS] COMMAND [ARGS]...
```

**Options**:

* `--install-completion`: Install completion for the current shell.
* `--show-completion`: Show completion for the current shell, to copy it or customize the installation.
* `--help`: Show this message and exit.

**Commands**:

* `decode`: Save thumbnail image from thumbhash
* `encode`: Get thumbhash from image

### `thumbhash decode`

Save thumbnail image from thumbhash

**Usage**:

```console
$ thumbhash decode [OPTIONS] IMAGE_PATH HASH
```

**Arguments**:

* `IMAGE_PATH`: The path where the image created from the hash will be saved [required]
* `HASH`: The base64-encoded thumbhash [required]

**Options**:

* `-s, --size INTEGER RANGE`: The base size of the output image [default: 32; x>=1]
* `--saturation FLOAT`: The saturation boost factor to use [default: 1.25]
* `--help`: Show this message and exit.

### `thumbhash encode`

Get thumbhash from image

**Usage**:

```console
$ thumbhash encode [OPTIONS] IMAGE_PATH
```

**Arguments**:

* `IMAGE_PATH`: The path of the image to convert [required]

**Options**:

* `--help`: Show this message and exit.


## Contributing

See [Contributing documentation](./.github/CONTRIBUTING.md)

## License

`thumbhash-python` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "thumbhash"
name = "thumbhash-python"
description = 'A Python implementation of the Thumbhash image placeholder generation algorithm.'
readme = "README.md"
requires-python = ">=3.7"
Expand Down Expand Up @@ -56,13 +56,17 @@ dev = [
"types-Pillow >=9.0.0"
]

[project.scripts]
thumbhash = "thumbhash.cli:main"

[tool.hatch.version]
path = "thumbhash/__version__.py"

[tool.hatch.build.targets.sdist]
exclude = [
"/.github",
"/.vscode",
"/scripts"
]

[tool.isort]
Expand Down Expand Up @@ -107,3 +111,8 @@ ignore = [

[tool.ruff.isort]
known-third-party = ["thumbhash"]

[tool.pyright]
reportUnknownMemberType=false
reportUnknownVariableType=false
reportUnknownArgumentType=false
14 changes: 14 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from thumbhash.cli import app
from typer.testing import CliRunner

from tests.data import ENCODE_DATA_TEST

runner = CliRunner()


def test_encode() -> None:
for IMAGE_PATH, THUMBHASH in ENCODE_DATA_TEST.items():
result = runner.invoke(app, ["encode", str(IMAGE_PATH)])

assert result.exit_code == 0
assert f"Thumbhash (base64): {THUMBHASH}" in result.stdout
3 changes: 3 additions & 0 deletions thumbhash/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cli import main

main()
55 changes: 55 additions & 0 deletions thumbhash/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from pathlib import Path
from typing import Any

import typer
from rich import print
from thumbhash import image_to_thumbhash, thumbhash_to_image

app = typer.Typer()


@app.command()
def encode(
image_path: Path = typer.Argument(
...,
help="The path of the image to convert",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
resolve_path=True,
)
) -> None:
"""
Get thumbhash from image
"""
hash = image_to_thumbhash(image_path)

print(f"Thumbhash (base64): [green]{hash}[/green]")


@app.command()
def decode(
image_path: Path = typer.Argument(
...,
help="The path where the image created from the hash will be saved",
file_okay=True,
dir_okay=False,
resolve_path=True,
),
hash: str = typer.Argument(..., help="The base64-encoded thumbhash"),
size: int = typer.Option(
32, "--size", "-s", help="The base size of the output image", min=1
),
saturation: float = typer.Option(1.25, help="The saturation boost factor to use"),
) -> None:
"""
Save thumbnail image from thumbhash
"""
image = thumbhash_to_image(hash, size, saturation)

image.save(image_path)


def main() -> Any:
return app()
18 changes: 7 additions & 11 deletions thumbhash/encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,18 @@ def image_to_thumbhash(
image: Union[str, bytes, Path, BinaryIO],
) -> str:
m_image = exif_transpose(Image.open(image)).convert("RGBA")
width, height = m_image.size

scale = 100 / max(width, height)

image_resized = m_image.resize(size=(round(width * scale), round(height * scale)))
m_image.close()
m_image.thumbnail((100, 100))

red_band = image_resized.getdata(band=0)
green_band = image_resized.getdata(band=1)
blue_band = image_resized.getdata(band=2)
alpha_band = image_resized.getdata(band=3)
red_band = m_image.getdata(band=0)
green_band = m_image.getdata(band=1)
blue_band = m_image.getdata(band=2)
alpha_band = m_image.getdata(band=3)
rgb_data = list(
chain.from_iterable(zip(red_band, green_band, blue_band, alpha_band))
)
width, height = image_resized.size
image_resized.close()
width, height = m_image.size
m_image.close()

hash = rgba_to_thumbhash(width, height, rgb_data)

Expand Down

0 comments on commit 89aa9a6

Please sign in to comment.