Commit 5c5f2252 authored by inkhey's avatar inkhey
Browse files

Merge branch 'feature/filtered_avatar_theme' into 'master'

Refactor pioupiou to handle image variation filter + add extra iptc keyword filter using exiftool

Closes #1

See merge request !2
parents 0b673bda 8655eacb
Pipeline #352755 passed with stage
in 35 seconds
## Unreleased
### Feat
- handle image variation filter + add extra iptc keyword filter using exiftool
### BREAKING CHANGE
- Refs:#1
## 0.7.0 (2020-10-17)
- better toolings (pre-commit, etc…)
......
......@@ -8,6 +8,7 @@
from PIL import Image
from pioupiou.chooser import Chooser
from pioupiou.exceptions import EmptyLayer
from pioupiou.exceptions import ImageGivenShouldHaveAlphaLayer
from pioupiou.exceptions import UnsupportedImageMode
from pioupiou.utils import generate_name
......@@ -91,9 +92,15 @@ def save_on_disk(self, avatar: Image, path: str) -> str:
# Low Level API
@dataclass
class ImageVariation(object):
path: str
metadata: typing.Dict[str, typing.Any]
class LayerInterface(Protocol):
level: int
variation_images_paths: typing.List[str]
image_variation_list: typing.List[ImageVariation]
name: str
def get_random_image(
......@@ -102,30 +109,73 @@ def get_random_image(
pass
class ImageVariationFilter(Protocol):
def __call__(
self, image_variation_list: typing.List[ImageVariation]
) -> typing.List[ImageVariation]:
...
class NoopImageVariationFilter(ImageVariationFilter):
def __call__(
self, image_variation_list: typing.List[ImageVariation]
) -> typing.List[ImageVariation]:
return image_variation_list
DEFAULT_IMAGE_VARIATION_FILTER = NoopImageVariationFilter()
@dataclass
class BaseLayer(LayerInterface):
"""
An layer is a part of an avatar theme, a layer
:param variation_images_paths: paths of different images possible for this layer, a
layer as differents image variations
:param level: level of layer, higher mean image will apply be over others.
:param images: path of image
A layer is a part of an avatar theme, a layer contains multiple image variations.
"""
level: int
variation_images_paths: typing.List[str]
name: str
level: int # level of layer, higher mean image will apply be over others.
image_variation_list: typing.List[
ImageVariation
] # list of different images available for this layer
name: str # Name of the layer
@classmethod
def create_layer_from_paths(
cls, level: int, image_variation_paths: typing.List[str], name: str
) -> "BaseLayer":
image_variation_list = cls._generate_image_variation_list(image_variation_paths)
return BaseLayer(level=level, image_variation_list=image_variation_list, name=name)
@classmethod
def _generate_image_variation_list(
cls, image_variation_paths: typing.List[str]
) -> typing.List[ImageVariation]:
image_variation_list = [] # type: typing.List[ImageVariation]
for path in image_variation_paths:
metadata = cls._get_image_variation_metadata(path)
image_variation_list.append(ImageVariation(path=path, metadata=metadata))
return image_variation_list
@classmethod
def _get_image_variation_metadata(cls, path: str) -> typing.Dict[str, typing.Any]:
return {"path": path}
def get_random_image(
self, chooser: Chooser = DEFAULT_CHOOSER, allow_no_alpha_layer: bool = False
self,
chooser: Chooser = DEFAULT_CHOOSER,
allow_no_alpha_layer: bool = False,
image_variation_filter: ImageVariationFilter = DEFAULT_IMAGE_VARIATION_FILTER,
) -> Image:
"""Get random image from layer based on current seed"""
assert self.variation_images_paths
chosen_path = chooser.choice(self.variation_images_paths)
filtered_image_variation = image_variation_filter(self.image_variation_list)
if not filtered_image_variation:
raise EmptyLayer("No valid image variation found for this layer")
chosen_image_variation = chooser.choice(filtered_image_variation) # type: ImageVariation
logging.getLogger(LOGGER_NAME).debug(
'open "{}" for layer "{}" of level "{}"'.format(chosen_path, self.name, self.level)
'open "{}" for layer "{}" of level "{}"'.format(
chosen_image_variation, self.name, self.level
)
)
image = Image.open(chosen_path)
image = Image.open(chosen_image_variation.path)
# INFO - GM - 03/07/2019 - We do not support image without alpha layer unless
if image.mode not in PILLOW_ALPHA_MODES:
if not allow_no_alpha_layer:
......@@ -151,20 +201,30 @@ class AvatarTheme(AvatarGeneratorInterface):
chooser: Chooser = random.Random()
name: str = generate_name()
def generate_avatar(self, token: str) -> Image:
def generate_avatar(
self,
token: str,
image_variation_filter: ImageVariationFilter = DEFAULT_IMAGE_VARIATION_FILTER,
) -> Image:
"""
Generate avatar Image by obtaining layer and applying them
:param token: token used as seed to decide layer variation to use.
:param image_variation_filter: Filter method to filter image variation
"""
sorted_layers = sorted(self.layers, key=lambda x: x.level)
self.chooser.seed(token + sorted_layers[0].name)
current_image = sorted_layers[0].get_random_image(
allow_no_alpha_layer=True, chooser=self.chooser
allow_no_alpha_layer=True,
chooser=self.chooser,
image_variation_filter=image_variation_filter,
)
for layer in sorted_layers[1:]:
self.chooser.seed(token + layer.name)
current_image = Image.alpha_composite(
current_image, layer.get_random_image(chooser=self.chooser)
current_image,
layer.get_random_image(
chooser=self.chooser, image_variation_filter=image_variation_filter
),
)
return current_image
......@@ -203,15 +263,17 @@ def __init__(
self.layers_name = layers_name
layers = []
for layer_number, layer_name in enumerate(layers_name):
file_list = self._generate_file_list(layer_name, folder_path)
assert file_list
image_variation_paths = self._generate_paths_from_folder(layer_name, folder_path)
assert image_variation_paths
layers.append(
layer_type(level=layer_number, variation_images_paths=file_list, name=layer_name)
layer_type.create_layer_from_paths(
level=layer_number, image_variation_paths=image_variation_paths, name=layer_name
)
)
assert layers
super().__init__(layers, chooser, name=name)
def _generate_file_list(self, layer_name, folder_path) -> typing.List[str]:
def _generate_paths_from_folder(self, layer_name, folder_path) -> typing.List[str]:
return sorted(
glob.glob(
LAYER_FILE_IMAGE_PATTERN.format(
......
......@@ -8,3 +8,7 @@ class ImageGivenShouldHaveAlphaLayer(PiouPiouException):
class UnsupportedImageMode(PiouPiouException):
pass
class EmptyLayer(PiouPiouException):
pass
......@@ -6,11 +6,11 @@
class TestAvatarTheme(object):
def test_avatar_theme__ok__nominal_case(self):
layer1 = BaseLayer(
level=0, name="star", variation_images_paths=["tests/test_img/black_background.png"]
layer1 = BaseLayer.create_layer_from_paths(
level=0, name="star", image_variation_paths=["tests/test_img/black_background.png"]
)
layer2 = BaseLayer(
level=1, name="star", variation_images_paths=["tests/test_img/white_alpha_star.png"]
layer2 = BaseLayer.create_layer_from_paths(
level=1, name="star", image_variation_paths=["tests/test_img/white_alpha_star.png"]
)
avatar_theme = AvatarTheme(layers=[layer1, layer2])
......
......@@ -6,23 +6,23 @@
class TestLayer(object):
def test_unit__layer__ok__nominal_case(self):
layer = BaseLayer(
level=0, name="star", variation_images_paths=["tests/test_img/white_alpha_star.png"]
layer = BaseLayer.create_layer_from_paths(
level=0, name="star", image_variation_paths=["tests/test_img/white_alpha_star.png"]
)
image = layer.get_random_image(allow_no_alpha_layer=False)
assert image.format == "PNG"
assert image.mode == "RGBA"
def test_unit__layer__err__image_should_have_alpha_layer(self):
layer = BaseLayer(
level=0, name="star", variation_images_paths=["tests/test_img/black_background.png"]
layer = BaseLayer.create_layer_from_paths(
level=0, name="star", image_variation_paths=["tests/test_img/black_background.png"]
)
with pytest.raises(ImageGivenShouldHaveAlphaLayer):
layer.get_random_image(allow_no_alpha_layer=False)
def test_unit__layer__err__ok__allow_alpha_laver(self):
layer = BaseLayer(
level=0, name="star", variation_images_paths=["tests/test_img/black_background.png"]
layer = BaseLayer.create_layer_from_paths(
level=0, name="star", image_variation_paths=["tests/test_img/black_background.png"]
)
image = layer.get_random_image(allow_no_alpha_layer=True)
assert image.mode == "RGBA"
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