Commit 3d78d4fb authored by inkhey's avatar inkhey

Add chooser mecanism with hashlib support

parent 715fd611
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks.git
sha: v2.2.1
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
- id: check-yaml
- id: debug-statements
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- repo: https://github.com/asottile/seed-isort-config
rev: v1.8.0
......@@ -36,12 +36,12 @@ repos:
args: [-l 100]
files: 'tests'
- repo: https://github.com/pre-commit/pre-commit-hooks.git
sha: v2.2.1
rev: v2.5.0
hooks:
- id: flake8
files: 'pioupiou'
- repo: https://github.com/pre-commit/pre-commit-hooks.git
sha: v2.2.1
rev: v2.5.0
hooks:
- id: flake8
files: 'tests'
......@@ -15,7 +15,9 @@ This generator rely on Pillow library to do image processing.
### From pypi
`pip install pioupiou`
~~~sh
$ pip install pioupiou
~~~
### From source
......@@ -31,29 +33,65 @@ To test it, you can simply use sample in example:
Cat avatar [by David Revoy](http://www.peppercarrot.com):
from pioupiou import FolderAvatarTheme
theme = FolderAvatarTheme(folder_path="sample/cat_revoy", layers_name=["body", "fur","eyes","mouth","accessorie"])
avatar = theme.generate_avatar(token="just a random string")
theme.save_on_disk(avatar, path="/tmp/saved_file.png")
~~~python
from pioupiou import FolderAvatarTheme
theme = FolderAvatarTheme(folder_path="sample/cat_revoy", layers_name=["body", "fur","eyes","mouth","accessorie"])
avatar = theme.generate_avatar(token="just a random string")
theme.save_on_disk(avatar, path="/tmp/saved_file.png")
~~~
Bird avatar [by David Revoy](http://www.peppercarrot.com):
from pioupiou import FolderAvatarTheme
theme = FolderAvatarTheme(
"sample/bird_revoy",
layers_name=["tail", "hoop", "body", "wing", "eyes", "bec", "accessorie"],
)
avatar = theme.generate_avatar(token="just a random string")
theme.save_on_disk(avatar, path="/tmp/saved_file.png")
~~~python
from pioupiou import FolderAvatarTheme
theme = FolderAvatarTheme(
"sample/bird_revoy",
layers_name=["tail", "hoop", "body", "wing", "eyes", "bec", "accessorie"],
)
avatar = theme.generate_avatar(token="just a random string")
theme.save_on_disk(avatar, path="/tmp/saved_file.png")
~~~
Monster avatar [by Andreas Gohr's](https://www.splitbrain.org/blog/2007-01/20_monsterid_as_gravatar_fallback):
from pioupiou import FolderAvatarTheme
theme = FolderAvatarTheme(
"sample/monster_id",
layers_name=["legs", "hair", "arms", "body", "eyes", "mouth"],
)
avatar = theme.generate_avatar(token="just a random string")
theme.save_on_disk(avatar, path="/tmp/saved_file.png")
~~~python
from pioupiou import FolderAvatarTheme
theme = FolderAvatarTheme(
"sample/monster_id",
layers_name=["legs", "hair", "arms", "body", "eyes", "mouth"],
)
avatar = theme.generate_avatar(token="just a random string")
theme.save_on_disk(avatar, path="/tmp/saved_file.png")
~~~
## Configure Chooser
To generate avatar, pioupiou provide tools to help us use the algorithm you want.
Default behaviour is to rely on `random.Random()`, but pioupiou is made to support any type of chooser.
To do this you just need to configure chooser in your `AvatarTheme`:
~~~python
from pioupiou import FolderAvatarTheme
from random import Random
theme = FolderAvatarTheme(
folder_path="sample/cat_revoy",
layers_name=["body", "fur","eyes","mouth","accessorie"],
chooser=Random() # same as default, don't change anything
)
~~~
You can use any hashlib algorithm you want from `hashlib` as chooser, using `pioupiou.chooser.HashLibChooser`.
It will do choice using modulo.
~~~python
from pioupiou import FolderAvatarTheme
import hashlib
from pioupiou.chooser import HashLibChooser
theme = FolderAvatarTheme(
folder_path="sample/cat_revoy",
layers_name=["body", "fur","eyes","mouth","accessorie"],
chooser=HashLibChooser(hashlib.sha256()) # hashlib.sha256() is just an example, hashlib.md5() should also work for example.
)
~~~
You can also implement `pioupiou.chooser.Chooser` and use a custom chooser.
\ No newline at end of file
import glob
import logging
import random
from random import Random
import typing
from PIL import Image
from pioupiou.chooser import Chooser
from pioupiou.exceptions import ImageGivenShouldHaveAlphaLayer
from pioupiou.exceptions import UnsupportedImageMode
......@@ -28,11 +28,11 @@ class Layer(object):
self.name = name
def get_random_image(
self, random_generator: Random = random, allow_no_alpha_layer: bool = False
self, chooser: Chooser = random, allow_no_alpha_layer: bool = False
) -> Image:
"""Get random image from layer based on current seed"""
assert self.variations_images_paths
chosen_path = random_generator.choice(self.variations_images_paths)
chosen_path = chooser.choice(self.variations_images_paths)
logging.getLogger(LOGGER_NAME).debug(
'open "{}" for layer "{}" of level "{}"'.format(chosen_path, self.name, self.level)
)
......@@ -57,23 +57,23 @@ class Layer(object):
class AvatarTheme(object):
def __init__(self, layers: typing.List[Layer]):
def __init__(self, layers: typing.List[Layer], chooser: typing.Optional[Chooser] = None):
self.layers = layers
self.rng = Random()
self.chooser = chooser or random.Random()
def generate_avatar(self, token: str) -> Image:
"""
Generate avatar Image by obtaining layer and applying them
:param token: token used as seed to decide layer variation to use.
"""
self.rng.seed(token)
self.chooser.seed(token)
sorted_layers = sorted(self.layers, key=lambda x: x.level)
current_image = sorted_layers[0].get_random_image(
allow_no_alpha_layer=True, random_generator=self.rng
allow_no_alpha_layer=True, chooser=self.chooser
)
for layer in sorted_layers[1:]:
current_image = Image.alpha_composite(
current_image, layer.get_random_image(random_generator=self.rng)
current_image, layer.get_random_image(chooser=self.chooser)
)
return current_image
......@@ -93,7 +93,7 @@ class FolderAvatarTheme(AvatarTheme):
Theme based on folder allowing easy setup of theme.
"""
def __init__(self, folder_path: str, layers_name: typing.List[str]):
def __init__(self, folder_path: str, layers_name: typing.List[str], chooser: Chooser = None):
"""
:param folder_path: path of folder where different layers are available
......@@ -116,4 +116,4 @@ class FolderAvatarTheme(AvatarTheme):
Layer(level=layer_number, variation_images_paths=file_list, name=layer_name)
)
assert layers
super().__init__(layers)
super().__init__(layers, chooser)
from abc import ABC
from abc import abstractmethod
import hashlib
import typing
class Chooser(ABC):
"""
Interface for chooser,
chooser does one thing: they choose in a sequence item according to a seed given
"""
@abstractmethod
def seed(self, seed: str) -> None:
"""set a seed in chooser"""
pass
@abstractmethod
def choice(self, sequence: typing.Sequence[typing.Any]) -> typing.Any:
"""Choose item in list"""
pass
class HashAlgorithm(ABC):
"""Hash Algorithm Interface for HashLibChooser """
@abstractmethod
def hexdigest(self) -> str:
pass
@abstractmethod
def update(self, seed: typing.Any):
pass
class HashLibChooser(Chooser):
"""
Generic class to use any hashlib algorithm for chooser
"""
def __init__(self, hash_algorithm: HashAlgorithm = None):
"""
:param hash_algorithm: should be a hashlib algorithm like
"""
self.hash_algorithm = hash_algorithm or hashlib.sha256() # type: HashAlgorithm
self.hexdigest = self.hash_algorithm.hexdigest() # type: str
def seed(self, seed: str):
encoded_seed = seed.encode("utf-8")
self.hash_algorithm.update(encoded_seed)
self.hexdigest = self.hash_algorithm.hexdigest()
def choice(self, sequence: typing.Sequence[typing.Any]):
hexdigest_as_number = int(self.hexdigest, 16)
sequence_len = len(sequence)
index = (hexdigest_as_number % sequence_len) - 1
return sequence[index]
import hashlib
import os
import typing
......@@ -6,6 +7,7 @@ from PIL import ImageChops
import pytest
from pioupiou import FolderAvatarTheme
from pioupiou.chooser import HashLibChooser
class TestFolderAvatarTheme(object):
......@@ -41,7 +43,7 @@ class TestFolderAvatarTheme(object):
),
],
)
def test_avatar_generation_on_disk(
def test_avatar_generation_on_disk_default_random_chooser(
self,
folder_path: str,
layers_name: typing.List[str],
......@@ -63,3 +65,59 @@ class TestFolderAvatarTheme(object):
with Image.open(compared_image_path) as compared_avatar:
diff = ImageChops.difference(avatar, compared_avatar)
assert not diff.getbbox()
@pytest.mark.parametrize(
"folder_path, layers_name, token, saved_file_path, compared_image_path, height, width",
[
(
"sample/cat_revoy",
["body", "fur", "eyes", "mouth", "accessorie"],
"just a random string",
"/tmp/saved_cat_avatar.png",
"tests/test_img_sha256/saved_cat_avatar.png",
256,
256,
),
(
"sample/bird_revoy",
["tail", "hoop", "body", "wing", "eyes", "bec", "accessorie"],
"just a random string",
"/tmp/saved_bird_avatar.png",
"tests/test_img_sha256/saved_bird_avatar.png",
256,
256,
),
(
"sample/monster_id",
["legs", "hair", "arms", "body", "eyes", "mouth"],
"just a random string",
"/tmp/saved_monster_avatar.png",
"tests/test_img_sha256/saved_monster_avatar.png",
120,
120,
),
],
)
def test_avatar_generation_on_disk__sha256_chooser(
self,
folder_path: str,
layers_name: typing.List[str],
token: str,
saved_file_path: str,
compared_image_path: str,
height: int,
width: int,
):
chooser = HashLibChooser(hash_algorithm=hashlib.sha256())
theme = FolderAvatarTheme(folder_path=folder_path, layers_name=layers_name, chooser=chooser)
avatar = theme.generate_avatar(token=token)
theme.save_on_disk(avatar, path=saved_file_path)
assert os.path.exists(saved_file_path) is True
assert os.path.getsize(saved_file_path) > 0
with Image.open(saved_file_path) as avatar:
assert avatar.height == height
assert avatar.width == width
# INFO - G.M - 06/07/2019 - Image should be exactly same as processed one in tests
with Image.open(compared_image_path) as compared_avatar:
diff = ImageChops.difference(avatar, compared_avatar)
assert not diff.getbbox()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment