# SPDX-License-Identifier: GPL-3.0
# Copyright (c) 2014-2023 William Edwards <shadowapex@gmail.com>, Benjamin Bean <superman2k5@gmail.com>
#
# fusion Module to fuse the face and body of two sprites.
# Based on Pokemon Fusion by Alex Onsager
# http://www.alexonsager.net/blog/2013/06/04/behind-the-scenes-pokemon-fusion/
#
# Note: this script, in its current state, is non-functional and the Tuxemon selected here
# serve only as examples of potential fusions.
from __future__ import annotations
from typing import Any, Mapping, Optional, Tuple
try:
from PIL import Image
except ImportError:
Image = Any
import json
[docs]class Body:
"""
A class that holds data for use with fusing two sprites together.
Example:
Two Tuxemon can be fused by joining the face of one with the
body of another.
>>> sapsnap = Body()
>>> # Load the sprite data from a json file
>>> sapsnap.load('fusion/Sapsnap.json')
>>>
>>> vivitron = Body()
>>> # Load the sprite data from a json file
>>> vivitron.load('fusion/Vivitron.json')
>>>
>>> # Fuse the sprites.
>>> fuse(body=sapsnap, face=vivitron)
>>> fuse(body=vivitron, face=sapsnap)
"""
face_image: Image
body_image: Image
def __init__(self) -> None:
# Name properties
self.prefix = "" # A name prefix to use when fusing sprites
self.suffix = "" # A name suffix to use when fusing sprites
# The full name of the sprite when you concat prefix + suffix
self.name = ""
# Face Properties
self.face_image_path = "" # The path to the face image to use.
# The face size can be automatically obtained through
# self.get_face_size()
self.face_size = (0, 0)
# The head size differs from the face size to take beaks,
# etc. into account.
self.head_size = (0, 0)
# The center of the face.
self.face_center = (0, 0)
# Body properties
# The path to the body image to use.
self.body_image_path = ""
# The center of the face on the body.
self.face_position = (0, 0)
# Colors
self.primary_colors = [
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
] # 5 primary colors of the sprite
self.secondary_colors = [
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
] # 5 secondary colors of the sprite
self.tertiary_colors = [
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
] # 5 tertiary colors of the sprite
[docs] def get_face_size(self) -> Tuple[int, int]:
"""
Obtains the size of the face image in pixels.
It also sets the instance's face_size to the returned value.
Returns:
A tuple (x, y) of the face size in pixels.
"""
img = self.face_image
img = img.convert("RGBA")
self.face_size = img.getdata().size
return self.face_size
[docs] def to_json(self) -> str:
"""
Converts the current instance to a dictionary and converts it to json.
Returns:
A json string of the current instance.
"""
body_dict = self.__dict__
del body_dict["body_image"]
del body_dict["face_image"]
return json.dumps(body_dict)
[docs] def save(self, filename: Optional[str] = None) -> None:
"""
Saves the current instance and all its properties to a json file.
Parameters:
filename: The path to the file to save.
"""
if not filename:
filename = "fusion/%s.json" % self.name
output = self.to_json()
with open(filename, "w") as f:
f.write(output)
[docs] def load(self, json_data: str, file: bool = True) -> None:
"""
Loads and sets all the properties to the properties in a json.
Parameters:
json_data: The string of json text or the file path to a json file
to load.
file: True or false value of whether or not "json_data" is a file
path.
Example:
>>> sapsnap = Body()
>>> sapsnap.load('fusion/Sapsnap.json')
"""
# If "file" is set to true, then assume that json_data is a path to a
# file containing json.
if file:
with open(json_data) as f:
json_data = "".join(f.readlines())
# Load the json data and convert it to a dictionary.
body_dict = json.loads(json_data)
# Set the name from the json data
self.prefix = body_dict["prefix"]
self.suffix = body_dict["suffix"]
self.name = body_dict["name"]
# Set the face properties from the json data
self.face_image_path = body_dict["face_image_path"]
self.face_size = body_dict["face_size"]
self.head_size = body_dict["head_size"]
self.face_center = body_dict["face_center"]
# Set the body properties from the json data
self.body_image_path = body_dict["body_image_path"]
self.face_position = body_dict["face_position"]
# Set the _color properties from the json data
self.primary_colors = body_dict["primary_colors"]
self.secondary_colors = body_dict["secondary_colors"]
self.tertiary_colors = body_dict["tertiary_colors"]
# Load the image files.
self.body_image = Image.open(self.body_image_path)
self.face_image = Image.open(self.face_image_path)
[docs] def get_state(self) -> Optional[Mapping[str, Any]]:
if self.name:
return self.__dict__
return None
[docs] def set_state(self, save_data: Optional[Mapping[str, Any]]) -> None:
# TODO: There's no point optimising this until Body is actually used.
if save_data:
for attr, value in save_data.items():
setattr(self, attr, value)
[docs]def replace_color(
image: Image,
original_color: Tuple[int, int, int],
replacement_color: Tuple[int, int, int],
) -> Image:
"""
Replaces an RGB color in an image with a different RGB _color.
Parameters:
image: A PIL Image() object of the image to replace colors.
original_color: A tuple of the RGB (r, g, b) value of the color to
replace.
replacement_color: A tuple of the RGB (r, g, b) value of the
new color.
Returns:
A PIL Image() object of the image with the given colors replaced.
"""
img = image.convert("RGBA")
datas = img.getdata()
r = original_color[0]
g = original_color[1]
b = original_color[2]
new_r = replacement_color[0]
new_g = replacement_color[1]
new_b = replacement_color[2]
newData = []
for item in datas:
if item[0] == r and item[1] == g and item[2] == b:
newData.append((new_r, new_g, new_b, 255))
else:
newData.append(item)
img.putdata(newData)
return img
[docs]def fuse(
body: Body,
face: Body,
save: bool = True,
filename: Optional[str] = None,
) -> Image:
"""Fuses two sprites together given a body and a face.
The resulting body will take on the colors of the face.
Parameters:
body: A Body() instance of the body that will be used in the end
result.
face: A Body() instance of the face that will be used in the end
result.
save: True or false value of whether or not to save the resulting
fusion to a file.
filename: If saving the result, specify the filename to save the
resulting image.
Returns:
A PIL Image() object of the fused sprites.
Example:
>>> sapsnap = Body()
>>> sapsnap.load('fusion/Sapsnap.json')
>>>
>>> vivitron = Body()
>>> vivitron.load('fusion/Vivitron.json')
>>>
>>> # Fuse the sprites.
>>> fuse(body=sapsnap, face=vivitron)
>>> fuse(body=vivitron, face=sapsnap)
"""
# Create a working copy of the body image so we don't alter the
# original sprite.
body_image = body.body_image.copy()
# Replace the _color of the body with the colors of the face.
for i, _ in enumerate(body.primary_colors):
body_image = replace_color(
body_image,
body.primary_colors[i],
face.primary_colors[i],
)
body_image = replace_color(
body_image,
body.secondary_colors[i],
face.secondary_colors[i],
)
body_image = replace_color(
body_image,
body.tertiary_colors[i],
face.tertiary_colors[i],
)
# Set a scale for the images so we can resize them.
# Scaling results in a better image result.
scale = 4
# Scale the images
body_image = body_image.resize(
(
body_image.getdata().size[0] * scale,
body_image.getdata().size[1] * scale,
)
)
face.face_image = face.face_image.resize(
(
face.face_image.getdata().size[0] * scale,
face.face_image.getdata().size[1] * scale,
)
)
# Update face size after we've performed our scaling.
face.face_size = (
face.face_image.getdata().size[0],
face.face_image.getdata().size[1],
)
# Scale the new face position.
body.face_position = (
((body.face_position[0] - 1) * scale) + 1,
((body.face_position[1] - 1) * scale) + 1,
)
# Compare the head size of the body and the face so we can scale
# the face to fit the body.
ratio_x = float(body.head_size[0]) / float(face.head_size[0])
ratio_y = float(body.head_size[1]) / float(face.head_size[1])
# Resize the head in ratio with the head size of the body
new_size = (
int(face.face_image.getdata().size[0] * ratio_x),
int(face.face_image.getdata().size[1] * ratio_y),
)
face.face_image = face.face_image.resize(new_size)
face.face_size = (
face.face_image.getdata().size[0],
face.face_image.getdata().size[1],
)
# Paste the face onto the body
position = (
body.face_position[0] - (face.face_size[0] / 2),
body.face_position[1] - (face.face_size[1] / 2),
)
body_image.paste(face.face_image, position, face.face_image)
# For some reason this looks really good.
# Scale the image back down using Image.ANTIALIAS
x = body_image.getdata().size[0] / (scale / 2)
y = body_image.getdata().size[1] / (scale / 2)
newsize = (x, y)
body_image = body_image.resize(newsize, Image.ANTIALIAS)
# Scale the image down further to its original size without ANTIALIAS
x /= scale / 2
y /= scale / 2
newsize = (x, y)
body_image = body_image.resize(newsize)
# Save the resulting image
if save:
if not filename:
filename = f"fusion/{body.prefix}{face.suffix}.png"
body_image.save(filename)
return body_image