diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..5682fad --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,72 @@ +name: "✨ Python" + +on: + workflow_dispatch: + pull_request: + paths: + - "**.py" + - "requirements.txt" + - "dev-requirements.txt" + - ".github/workflows/python.yml" + +permissions: + contents: read + pull-requests: read + +jobs: + build: + name: analyse + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: 3.12 + allow-prereleases: true + + - name: Install dependencies + run: | # $(git ls-files '*dev-requirements.txt') # $(git ls-files '*requirements.txt') + python -m pip install --upgrade pip + pip install -r scripts/requirements.txt + pip install -r scripts/dev-requirements.txt + + - name: Lint check with Ruff + run: | + if [ ${{ matrix.python-version }} == 3.7 ]; then + ruff check $(git ls-files '*.py') --target-version=py37 + fi + if [ ${{ matrix.python-version }} == 3.8 ]; then + ruff check $(git ls-files '*.py') --target-version=py38 + fi + if [ ${{ matrix.python-version }} == 3.9 ]; then + ruff check $(git ls-files '*.py') --target-version=py39 + fi + if [ ${{ matrix.python-version }} == 3.10 ]; then + ruff check $(git ls-files '*.py') --target-version=py310 + fi + if [ ${{ matrix.python-version }} == 3.11 ]; then + ruff check $(git ls-files '*.py') --target-version=py311 + fi + if [ ${{ matrix.python-version }} == 3.12 ]; then + ruff check $(git ls-files '*.py') --target-version=py312 + fi + + - name: Static analysis with MyPy + run: | + mypy $(git ls-files '*.py') --python-version ${{ matrix.python-version }} + + - name: Testing with Pytest + run: | + cd scripts + pytest diff --git a/assets/Personal/README.md b/assets/Personal/README.md index 0ff7e98..4cfa39a 100644 --- a/assets/Personal/README.md +++ b/assets/Personal/README.md @@ -1,8 +1,8 @@ # Personal Banner - - Here are all the available Personal's banner. the following table below this message is sorted by Date. | 📃 Filename | 🗓️ Date | 📔 Category | 📜 Description | | --- | --- | --- | --- | | `default-light` | Default | Default | Default Theme | + + \ No newline at end of file diff --git a/assets/Personal/default-light.webp b/assets/Personal/default-light.webp index 6cde06c..e49be82 100644 Binary files a/assets/Personal/default-light.webp and b/assets/Personal/default-light.webp differ diff --git a/assets/ReVancedManager/README.md b/assets/ReVancedManager/README.md index ffc1e1a..f0c043f 100644 --- a/assets/ReVancedManager/README.md +++ b/assets/ReVancedManager/README.md @@ -1,6 +1,4 @@ # ReVanced Manager Banner - - Here are all the available ReVanced Manager's banner. the following table below this message is sorted by Date. | 📃 Filename | 🗓️ Date | 📔 Category | 📜 Description | @@ -10,9 +8,13 @@ Here are all the available ReVanced Manager's banner. the following table below | `incubator-light` | Default | Reserved | Default Theme for Incubator | | `valentine-dark` | February 14th | Holiday | 💖 Valentine's Day | | `valentine-light` | February 14th | Holiday | 💖 Valentine's Day | +| `valentine-light-compose` | February 14th | Reserved | Compose variant of `valentine-light` | | `iwd-dark` | March 8th | International Day | ♀️ International Women's Day | | `iwd-light` | March 8th | International Day | ♀️ International Women's Day | | `imd-dark` | November 19th | International Day | ♂️ International Men's Day | | `imd-light` | November 19th | International Day | ♂️ International Men's Day | -| `valentine-dark` | July 10th | Personal | 💖 Happy Birthday | -| `valentine-light` | July 10th | Personal | 💖 Happy Birthday | +| `valentine-dark` | July 10th | Personal | 💖 Valentine's Day | +| `valentine-light` | July 10th | Personal | 💖 Valentine's Day | +| `valentine-light-compose` | July 10th | Reserved | Compose variant of `valentine-light` | + + \ No newline at end of file diff --git a/assets/ReVancedManager/default-dark.webp b/assets/ReVancedManager/default-dark.webp index e6f5419..9797658 100644 Binary files a/assets/ReVancedManager/default-dark.webp and b/assets/ReVancedManager/default-dark.webp differ diff --git a/assets/ReVancedManager/default-light.webp b/assets/ReVancedManager/default-light.webp index b484f0a..70da633 100644 Binary files a/assets/ReVancedManager/default-light.webp and b/assets/ReVancedManager/default-light.webp differ diff --git a/assets/ReVancedManager/imd-dark.webp b/assets/ReVancedManager/imd-dark.webp index b17bb42..4d7d527 100644 Binary files a/assets/ReVancedManager/imd-dark.webp and b/assets/ReVancedManager/imd-dark.webp differ diff --git a/assets/ReVancedManager/imd-light.webp b/assets/ReVancedManager/imd-light.webp index 10653d6..5ea3616 100644 Binary files a/assets/ReVancedManager/imd-light.webp and b/assets/ReVancedManager/imd-light.webp differ diff --git a/assets/ReVancedManager/incubator-light.webp b/assets/ReVancedManager/incubator-light.webp index 87164f0..edb1e9f 100644 Binary files a/assets/ReVancedManager/incubator-light.webp and b/assets/ReVancedManager/incubator-light.webp differ diff --git a/assets/ReVancedManager/iwd-dark.webp b/assets/ReVancedManager/iwd-dark.webp index bd8e3fd..4ab66c3 100644 Binary files a/assets/ReVancedManager/iwd-dark.webp and b/assets/ReVancedManager/iwd-dark.webp differ diff --git a/assets/ReVancedManager/iwd-light.webp b/assets/ReVancedManager/iwd-light.webp index 074ef29..3e26a18 100644 Binary files a/assets/ReVancedManager/iwd-light.webp and b/assets/ReVancedManager/iwd-light.webp differ diff --git a/assets/ReVancedManager/legacy/README.md b/assets/ReVancedManager/legacy/README.md deleted file mode 100644 index 4484e9a..0000000 --- a/assets/ReVancedManager/legacy/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# ReVanced Manager Banner - - -Here are all the legacy ReVanced Manager's banner. the following table below this message is sorted by Date. - -Reason for removal: Logo is not up-to-date - -| 📃 Filename | 🗓️ Date | 📔 Category | 📜 Description | -| --- | --- | --- | --- | -| `default-dark` | Default | Default | Default Theme | -| `default-light` | Default | Default | Default Theme | -| `incubator-light` | Default | Reserved | Default Theme for Incubator | -| `valentine-dark` | February 14th | Holiday | 💖 Valentine's Day | -| `valentine-light` | February 14th | Holiday | 💖 Valentine's Day | -| `iwd-dark` | March 8th | International Day | ♀️ International Women's Day | -| `iwd-light` | March 8th | International Day | ♀️ International Women's Day | -| `imd-dark` | November 19th | International Day | ♂️ International Men's Day | -| `imd-light` | November 19th | International Day | ♂️ International Men's Day | -| `valentine-dark` | July 10th | Personal | 💖 Happy Birthday | -| `valentine-light` | July 10th | Personal | 💖 Happy Birthday | diff --git a/assets/ReVancedManager/legacy/default-dark.webp b/assets/ReVancedManager/legacy/default-dark.webp deleted file mode 100644 index 29130dc..0000000 Binary files a/assets/ReVancedManager/legacy/default-dark.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/default-light.webp b/assets/ReVancedManager/legacy/default-light.webp deleted file mode 100644 index ca8c04f..0000000 Binary files a/assets/ReVancedManager/legacy/default-light.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/imd-dark.webp b/assets/ReVancedManager/legacy/imd-dark.webp deleted file mode 100644 index 41910a2..0000000 Binary files a/assets/ReVancedManager/legacy/imd-dark.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/imd-light.webp b/assets/ReVancedManager/legacy/imd-light.webp deleted file mode 100644 index 73f397b..0000000 Binary files a/assets/ReVancedManager/legacy/imd-light.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/incubator-light.png b/assets/ReVancedManager/legacy/incubator-light.png deleted file mode 100644 index a95a3bb..0000000 Binary files a/assets/ReVancedManager/legacy/incubator-light.png and /dev/null differ diff --git a/assets/ReVancedManager/legacy/iwd-dark.webp b/assets/ReVancedManager/legacy/iwd-dark.webp deleted file mode 100644 index 6f8528f..0000000 Binary files a/assets/ReVancedManager/legacy/iwd-dark.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/iwd-light.webp b/assets/ReVancedManager/legacy/iwd-light.webp deleted file mode 100644 index 1756b9b..0000000 Binary files a/assets/ReVancedManager/legacy/iwd-light.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/valentine-dark.webp b/assets/ReVancedManager/legacy/valentine-dark.webp deleted file mode 100644 index 1595b77..0000000 Binary files a/assets/ReVancedManager/legacy/valentine-dark.webp and /dev/null differ diff --git a/assets/ReVancedManager/legacy/valentine-light.webp b/assets/ReVancedManager/legacy/valentine-light.webp deleted file mode 100644 index 24d1f38..0000000 Binary files a/assets/ReVancedManager/legacy/valentine-light.webp and /dev/null differ diff --git a/assets/ReVancedManager/valentine-dark.webp b/assets/ReVancedManager/valentine-dark.webp index 4a99ecc..01ade7a 100644 Binary files a/assets/ReVancedManager/valentine-dark.webp and b/assets/ReVancedManager/valentine-dark.webp differ diff --git a/assets/ReVancedManager/valentine-light-compose.webp b/assets/ReVancedManager/valentine-light-compose.webp new file mode 100644 index 0000000..2eba67d Binary files /dev/null and b/assets/ReVancedManager/valentine-light-compose.webp differ diff --git a/assets/ReVancedManager/valentine-light.webp b/assets/ReVancedManager/valentine-light.webp index b6189d8..7479ba2 100644 Binary files a/assets/ReVancedManager/valentine-light.webp and b/assets/ReVancedManager/valentine-light.webp differ diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..6769e21 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..75ccb14 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,30 @@ +# `> Image manipulation` +Python script that's mostly used for image manipulation + +Support down to Python 3.9 + +## `> Image // Contributing` + +Thanks for consider contributing. +> [!IMPORTANT] +> This guide assume that you already installed Python 3.10 or higher and pytest. + +Running the script: +1. Clone the repository: `git@github.com:validcube/validcube.git && cd scripts` +2. Install the required dependencies: `pip3 install -r requirements.txt` +3. Run the script: `python3 convert_now.py` + +Developing the script: +1. Move into scripts directory: `cd scripts` +2. Install the required dependencies for development: `pip3 install -r dev-requirements.txt` +3. Lint check: `ruff .` +4. Static Analysis: `mypy .` +5. Test the script: `pytest` + +> [!WARNING] +> Ruff is a linter made using Rust, it's super fast, don't fall for it! + +Here are some tip when contributing: +* All commits must follows the [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) guidelines. +* [Signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) are highly recommended. +* This repository follows slight variation of [Google's Python style](https://google.github.io/styleguide/pyguide.html) guide **but not strictly enforced**. diff --git a/scripts/convert_now.py b/scripts/convert_now.py new file mode 100644 index 0000000..0a32618 --- /dev/null +++ b/scripts/convert_now.py @@ -0,0 +1,9 @@ +import os +from PIL import Image +from image import round_corners, export_to_webp + +for file in os.listdir("assets"): + print(f"🗿 Converting {file}") + img = Image.open(os.path.join("assets", file)) + img_rounded_corners = round_corners(img=img, corner_radius=24) + export_to_webp(img_rounded_corners, os.path.join("output", f"{file.strip('.png')}.webp"), "WEBP") diff --git a/scripts/dev-requirements.txt b/scripts/dev-requirements.txt new file mode 100644 index 0000000..d54217c --- /dev/null +++ b/scripts/dev-requirements.txt @@ -0,0 +1,4 @@ +pytest >= 7.4.2 +ruff >= 0.1.1 +mypy >= 1.6.1 +types-Pillow >= 10.1.0.0 diff --git a/scripts/image.py b/scripts/image.py new file mode 100644 index 0000000..78208eb --- /dev/null +++ b/scripts/image.py @@ -0,0 +1,152 @@ +import os +from PIL import Image, ImageOps, ImageDraw, ImageFilter + +if __name__ == "__main__": + pass #exit() + +def add_margin( + img: Image.Image, + margin_size: int +) -> Image.Image: + """Add margin to the input image. + + #### Args: + input_file_path: The path of the input image file. + margin_size: The size of the margin to be added. + + #### Returns: + The image with added margin. + + #### Raises: + ValueError: If the margin size is not greater than 0. + """ + if margin_size <= 0: + raise ValueError(f"🥞 The margin size must be greater than 0. Got {margin_size}.") + + border = (margin_size, margin_size, margin_size, margin_size) + img_with_border = ImageOps.expand(img, border=border) + print(f"🥞 Image size after adding margin: {img_with_border.size}") + return img_with_border + + +def round_corners( + img: Image.Image, + corner_radius: int +) -> Image.Image: + """Add rounded corners to the input image. + + #### Args: + img: The input image. + corner_radius: The radius of the rounded corners. + + #### Returns: + The image with rounded corners. + + #### Raises: + ValueError: If the corner radius is not greater than 0. + """ + if corner_radius <= 0: + raise ValueError(f"🥞 The corner radius must be greater than 0. Got {corner_radius}.") + + shape_mask = Image.new("L", img.size, 0) + draw = ImageDraw.Draw(shape_mask) + + draw.rectangle((corner_radius, 0, img.size[0] - corner_radius, img.size[1]), fill=255) + draw.rectangle((0, corner_radius, img.size[0], img.size[1] - corner_radius), fill=255) + draw.pieslice(((0.0, 0.0), (corner_radius * 2.0, corner_radius * 2.0)), 180, 270, fill=255) + draw.pieslice(((img.size[0] - corner_radius * 2.0, 0.0), (img.size[0], corner_radius * 2.0)), 270, 360, fill=255) + draw.pieslice(((img.size[0] - corner_radius * 2.0, img.size[1] - corner_radius * 2.0), (img.size[0], img.size[1])), 0, 90, fill=255) + draw.pieslice(((0.0, img.size[1] - corner_radius * 2.0), (corner_radius * 2.0, img.size[1])), 90, 180, fill=255) + + img_mask = Image.new("L", img.size, 255) + img_mask.paste(shape_mask) + img_rounded_corners = Image.composite(img.convert("RGBA"), Image.new("RGBA", img.size), img_mask) + + return img_rounded_corners + + +def add_glow( + img: Image.Image, + glow_color: tuple = (0, 0, 0), + max_alpha: float = 0, + blur_radius: int = 10, + padding: int = 50, +) -> Image.Image: + """Add glow effect to the input image. + + #### Args: + img: The input image. + glow_color: The color of the glow in RGB format. + max_alpha: The maximum alpha value of the glow color. + blur_radius: The radius of the blur effect for the glow. + padding: The number of pixels to add to each dimension of the image. + + #### Returns: + The image with added glow. + """ + new_size = (img.size[0] + padding, img.size[1] + padding) + + glow = Image.new("RGBA", new_size) + draw = ImageDraw.Draw(glow) + + for i in range(padding): + alpha = int(max_alpha * (1 - i / padding)) # Adjust alpha here + color = (glow_color[0], glow_color[1], glow_color[2], alpha) + draw.rectangle((i, i, new_size[0] - i, new_size[1] - i), outline=color) + + glow.paste(img, (int((new_size[0] - img.size[0]) / 2), int((new_size[1] - img.size[1]) / 2))) + blurred_glow = glow.filter(ImageFilter.GaussianBlur(blur_radius)) + + # Hacky stuff to fix the alpha channel pasted on top of the shadow + background = Image.new("RGBA", blurred_glow.size) + background.paste(blurred_glow, (0, 0), mask=blurred_glow) + background.paste(img, (int((new_size[0] - img.size[0]) / 2), int((new_size[1] - img.size[1]) / 2)), mask=img) + + return background + + +def export_to_webp( + image: Image.Image, + output_file_path: str = "output.webp", + format: str = "WEBP" + ) -> None: + """Export the input image in WebP format. + + #### Args: + image: The input image. + output_file_path: The path of the output file. + format: Bitmap format that + """ + image.save(output_file_path, format) + assert os.path.exists(output_file_path) + print(f"🥞 Image saved to {output_file_path}") + + +#input_file_path = "test_input.png" +# +## Configurable parameters +#margin_size = 16 +#corner_radius = 16 +#glow_color = (0, 0, 0) +#max_alpha = 0.5 +#blur_radius = 33 +#padding = 50 +# +#img = Image.open(input_file_path) +# +#img_rounded_corners = round_corners( +# img=img, +# corner_radius=corner_radius +#) +#img_with_soft_glow = add_glow( +# img_rounded_corners, +# glow_color=glow_color, +# max_alpha=max_alpha, +# blur_radius=blur_radius, +# padding=padding +#) +##img_with_border = add_margin( +## img=img_with_shadow, +## margin_size=margin_size +##) +#export_to_webp(img_with_soft_glow, "test_output.webp") diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..3df4d81 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +pillow >= 10.1.0 diff --git a/scripts/test/test_image.py b/scripts/test/test_image.py new file mode 100644 index 0000000..deed109 --- /dev/null +++ b/scripts/test/test_image.py @@ -0,0 +1,94 @@ +import os +import sys +import pytest +from PIL import Image +sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) +import image # noqa: E402 + +class TestAddMargin: + input_file_path = "test_input.png" + img = Image.open(input_file_path) + # Check if the output image has the correct size + + def test_add_margin_negative(self): + margin_size = -1 + img = Image.open(self.input_file_path) + with pytest.raises(ValueError): + image.add_margin(img, margin_size) + + + def test_add_margin_50(self): + margin_size = 50 + img = Image.open(self.input_file_path) + img_with_border = image.add_margin(img, margin_size) + assert img_with_border.size == ( + img.width + 2 * margin_size, + img.height + 2 * margin_size, + ) + + def test_add_margin_100(self): + margin_size = 100 + img = Image.open(self.input_file_path) + img_with_border = image.add_margin(img, margin_size) + assert img_with_border.size == ( + img.width + 2 * margin_size, + img.height + 2 * margin_size, + ) + + def test_add_margin_1000(self): + margin_size = 1000 + img = Image.open(self.input_file_path) + img_with_border = image.add_margin(img, margin_size) + assert img_with_border.size == ( + img.width + 2 * margin_size, + img.height + 2 * margin_size, + ) + + +class TestRoundCorner: + input_file_path = "test_input.png" + img = Image.open(input_file_path) + # Check if the output image has the correct size + + def test_corner_negative(self): + corner_radius = -1 + with pytest.raises(ValueError): + image.round_corners(self.img, corner_radius) + + def test_corner_precise(self): + corner_radius = 0.44322 + img_rounded_corners = image.round_corners(self.img, corner_radius) + assert img_rounded_corners.size == self.img.size + + def test_corner_8(self): + corner_radius = 8 + img_rounded_corners = image.round_corners(self.img, corner_radius) + assert img_rounded_corners.size == self.img.size + + def test_corner_16(self): + corner_radius = 16 + img_rounded_corners = image.round_corners(self.img, corner_radius) + assert img_rounded_corners.size == self.img.size + + def test_corner_24(self): + corner_radius = 24 + img_rounded_corners = image.round_corners(self.img, corner_radius) + assert img_rounded_corners.size == self.img.size + + +def test_export_to_webp(): + if os.path.exists("test_output.webp"): + os.remove("test_output.webp") + + input_file_path = "test_input.png" + + img = Image.open(input_file_path) + image.export_to_webp(img, "test_output.webp") + + # Check if the output file exists + assert os.path.exists("test_output.webp") + os.remove("test_output.webp") + + +if __name__ == "__main__": + pytest.main() diff --git a/scripts/test_input.png b/scripts/test_input.png new file mode 100644 index 0000000..5c8f4c8 Binary files /dev/null and b/scripts/test_input.png differ diff --git a/scripts/test_output.webp b/scripts/test_output.webp new file mode 100644 index 0000000..1c69d96 Binary files /dev/null and b/scripts/test_output.webp differ