Commit 8655eacb authored by inkhey's avatar inkhey
Browse files

feat: handle image variation filter + add extra iptc keyword filter using exiftool

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