From 89aa9a62fba65c3b51cb42688ee516a148ceccf2 Mon Sep 17 00:00:00 2001
From: Yohann MARTIN
Date: Wed, 5 Apr 2023 00:57:32 +0200
Subject: [PATCH] Added CLI + updated README
---
.github/CONTRIBUTING.md | 5 ++
README.md | 156 +++++++++++++++++++++++++++++++++++++++-
pyproject.toml | 11 ++-
tests/test_cli.py | 14 ++++
thumbhash/__main__.py | 3 +
thumbhash/cli.py | 55 ++++++++++++++
thumbhash/encode.py | 18 ++---
7 files changed, 248 insertions(+), 14 deletions(-)
create mode 100644 tests/test_cli.py
create mode 100644 thumbhash/__main__.py
create mode 100644 thumbhash/cli.py
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 6d6a09b..3a05111 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -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]'
diff --git a/README.md b/README.md
index f0d9735..e44ddaf 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,154 @@
-# thumbhash-python
-A Python implementation of the Thumbhash image placeholder generation algorithm.
+
+
+ ThumbHash for Python
+
+
+
Open-source, end-to-end encrypted tool to manage secrets and configs across your team, devices, and infrastructure.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# 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.
diff --git a/pyproject.toml b/pyproject.toml
index df0d548..7dc87c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"
@@ -56,6 +56,9 @@ dev = [
"types-Pillow >=9.0.0"
]
+[project.scripts]
+thumbhash = "thumbhash.cli:main"
+
[tool.hatch.version]
path = "thumbhash/__version__.py"
@@ -63,6 +66,7 @@ path = "thumbhash/__version__.py"
exclude = [
"/.github",
"/.vscode",
+ "/scripts"
]
[tool.isort]
@@ -107,3 +111,8 @@ ignore = [
[tool.ruff.isort]
known-third-party = ["thumbhash"]
+
+[tool.pyright]
+reportUnknownMemberType=false
+reportUnknownVariableType=false
+reportUnknownArgumentType=false
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..c7bcfdf
--- /dev/null
+++ b/tests/test_cli.py
@@ -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
diff --git a/thumbhash/__main__.py b/thumbhash/__main__.py
new file mode 100644
index 0000000..4e28416
--- /dev/null
+++ b/thumbhash/__main__.py
@@ -0,0 +1,3 @@
+from .cli import main
+
+main()
diff --git a/thumbhash/cli.py b/thumbhash/cli.py
new file mode 100644
index 0000000..da56249
--- /dev/null
+++ b/thumbhash/cli.py
@@ -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()
diff --git a/thumbhash/encode.py b/thumbhash/encode.py
index 05e72a4..72a6bb0 100644
--- a/thumbhash/encode.py
+++ b/thumbhash/encode.py
@@ -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)