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.

+

+ + +

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + + + MIT License + +

+ +# 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)