Skip to content

Commit

Permalink
Fun: Finally fix animated gifs
Browse files Browse the repository at this point in the history
This issue has existed since the very beginning of this module in
Rubbergoddess (and Rubbergod) codebase. Fairly recently (April 2022)
the issue of flickering GIFs has been fixed in Pillow 9.1.0 via PR 6128.

As the issue depends on the avatar of the user, it is not possible to
claim this fixes it 100 %. I do believe/hope it does.
  • Loading branch information
sinus-x committed Jun 8, 2023
1 parent 464863c commit ad577ba
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 168 deletions.
151 changes: 0 additions & 151 deletions fun/image_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,154 +73,3 @@ def shift_hue(arr, hout):
hsv[..., 0] = hout
rgb = ImageUtils.hsv_to_rgb(hsv)
return rgb

class GifConverter:
# Sourced from https://gist.github.com/egocarib/ea022799cca8a102d14c54a22c45efe0
_PALETTE_SLOTSET = set(range(256))

def __init__(self, img_rgba: Image.Image, alpha_treshold: int = 0):
self._img_rgba = img_rgba
self._alpha_treshold = alpha_treshold

def _process_pixels(self):
"""Set transparent pixels to the color palette index 0."""
self._transparent_pixels = set(
idx
for idx, alpha in enumerate(
self._img_rgba.getchannel(channel="A").getdata()
)
if alpha <= self._alpha_treshold
)

def _set_parsed_palette(self):
"""Parse the RGB palette color `tuple`s from the palette."""
palette = self._img_p.getpalette()
self._img_p_used_palette_idxs = set(
idx
for pal_idx, idx in enumerate(self._img_p_data)
if pal_idx not in self._transparent_pixels
)
self._img_p_parsedpalette = dict(
(idx, tuple(palette[idx * 3 : idx * 3 + 3]))
for idx in self._img_p_used_palette_idxs
)

def _get_similar_color_idx(self):
"""Return a palette index with the closest similar color."""
old_color = self._img_p_parsedpalette[0]
dict_distance = defaultdict(list)
for idx in range(1, 256):
color_item = self._img_p_parsedpalette[idx]
if color_item == old_color:
return idx
distance = sum(
(
abs(old_color[0] - color_item[0]), # Red
abs(old_color[1] - color_item[1]), # Green
abs(old_color[2] - color_item[2]), # Blue
)
)
dict_distance[distance].append(idx)
return dict_distance[sorted(dict_distance)[0]][0]

def _remap_palette_idx_zero(self):
"""Since the first color is used in the palette, remap it."""
free_slots = self._PALETTE_SLOTSET - self._img_p_used_palette_idxs
new_idx = free_slots.pop() if free_slots else self._get_similar_color_idx()
self._img_p_used_palette_idxs.add(new_idx)
self._palette_replaces["idx_from"].append(0)
self._palette_replaces["idx_to"].append(new_idx)
self._img_p_parsedpalette[new_idx] = self._img_p_parsedpalette[0]
del self._img_p_parsedpalette[0]

def _get_unused_color(self) -> tuple:
"""Return a color for the palette that does not collide with any other
already in the palette."""
used_colors = set(self._img_p_parsedpalette.values())
while True:
new_color = (randrange(256), randrange(256), randrange(256))
if new_color not in used_colors:
return new_color

def _process_palette(self):
"""Adjust palette to have the zeroth color set as transparent.
Basically, get another palette index for the zeroth color."""
self._set_parsed_palette()
if 0 in self._img_p_used_palette_idxs:
self._remap_palette_idx_zero()
self._img_p_parsedpalette[0] = self._get_unused_color()

def _adjust_pixels(self):
"""Convert the pixels into their new values."""
if self._palette_replaces["idx_from"]:
trans_table = bytearray.maketrans(
bytes(self._palette_replaces["idx_from"]),
bytes(self._palette_replaces["idx_to"]),
)
self._img_p_data = self._img_p_data.translate(trans_table)
for idx_pixel in self._transparent_pixels:
self._img_p_data[idx_pixel] = 0
self._img_p.frombytes(data=bytes(self._img_p_data))

def _adjust_palette(self):
"""Modify the palette in the new `Image`."""
unused_color = self._get_unused_color()
final_palette = chain.from_iterable(
self._img_p_parsedpalette.get(x, unused_color) for x in range(256)
)
self._img_p.putpalette(data=final_palette)

def process(self) -> Image.Image:
"""Return the processed mode `P` `Image`."""
self._img_p = self._img_rgba.convert(mode="P")
self._img_p_data = bytearray(self._img_p.tobytes())
self._palette_replaces = dict(idx_from=list(), idx_to=list())
self._process_pixels()
self._process_palette()
self._adjust_pixels()
self._adjust_palette()
self._img_p.info["transparency"] = 0
self._img_p.info["background"] = 0
return self._img_p

def create_animated_gif(
images: List[Image.Image], duration: Union[int, List[int]]
) -> Tuple[Image.Image, dict]:
"""If the image is a GIF, create an its thumbnail here."""
save_kwargs = dict()
new_images: List[Image] = []

for frame in images:
thumbnail: Image.Image = frame.copy()
thumbnail_rgba = thumbnail.convert(mode="RGBA")
thumbnail_rgba.thumbnail(size=frame.size, reducing_gap=3.0)
converter = GifConverter(img_rgba=thumbnail_rgba) # noqa: F821
thumbnail_p = converter.process()
new_images.append(thumbnail_p)

output_image = new_images[0]
save_kwargs.update(
format="GIF",
save_all=True,
optimize=False,
append_images=new_images[1:],
duration=duration,
disposal=2, # Other disposals don't work
loop=0,
)
return output_image, save_kwargs

def save_gif(
images: List[Image.Image], duration: Union[int, List[int]], save_file: str
):
"""Create a transparent GIF, with no problems with transparent pixel flashing
Does not work with partial alpha, which gets discarded and replaced by solid colors.
Parameters:
images: list of Pillow Image objects that compose the GIF frames
durations: an int or list of ints that describe the frame durations
save_file: A string, pathlib.Path or file object to save the file to.
"""
root_frame, save_args = create_animated_gif(images, duration) # noqa: F821
root_frame.save(save_file, **save_args)
41 changes: 24 additions & 17 deletions fun/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,13 @@ async def whip(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -226,12 +227,13 @@ async def spank(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=frame_duration,
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -268,12 +270,13 @@ async def pet(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=40,
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -310,12 +313,13 @@ async def hyperpet(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -355,12 +359,13 @@ async def bonk(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -437,12 +442,13 @@ async def lick(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -479,12 +485,13 @@ async def hyperlick(self, ctx, *, user: discord.Member = None):
frames[0].save(
image_binary,
format="GIF",
interlace=True,
save_all=True,
append_images=frames[1:],
duration=30,
loop=0,
transparency=0,
disposal=2,
background=255,
optimize=False,
)
image_binary.seek(0)
Expand Down Expand Up @@ -791,7 +798,7 @@ def get_pet_frames(avatar: Image.Image) -> List[Image.Image]:

for i in range(14):
img = "%02d" % (i + 1)
frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame_object = Image.open(DATA_DIR / f"pet/{img}.png")
frame.paste(frame_avatar, (35, 25 + vertical_offset[i]), frame_avatar)
frame.paste(frame_object, (10, 5), frame_object)
Expand All @@ -817,7 +824,7 @@ def get_hyperpet_frames(avatar: Image.Image) -> List[Image.Image]:
)
frame_object = Image.open(DATA_DIR / f"hyperpet/{img}.png")

frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame.paste(frame_avatar, (35, 25 + vertical_offset[i]), frame_avatar)
frame.paste(frame_object, (10, 5), frame_object)
frames.append(frame)
Expand All @@ -838,7 +845,7 @@ def get_bonk_frames(avatar: Image.Image) -> List[Image.Image]:
frame_avatar = avatar.resize((100, 100 - deformation[i]))
frame_object = Image.open(DATA_DIR / f"bonk/{img}.png")

frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame.paste(frame_avatar, (80, 60 + deformation[i]), frame_avatar)
frame.paste(frame_object, (10, 5), frame_object)
frames.append(frame)
Expand All @@ -860,7 +867,7 @@ def get_whip_frames(avatar: Image.Image) -> List[Image.Image]:
frame_avatar = avatar.resize((100 - deformation[i], 100))
frame_object = Image.open(DATA_DIR / f"whip/{img}.png").resize((150, 150))

frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame.paste(
frame_avatar, (135 + deformation[i] + translation[i], 25), frame_avatar
)
Expand All @@ -885,7 +892,7 @@ def get_spank_frames(avatar: Image.Image) -> List[Image.Image]:
)
frame_object = Image.open(DATA_DIR / f"spank/{img}.png").resize((100, 100))

frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame.paste(frame_object, (10, 15), frame_object)
frame.paste(
frame_avatar, (80 - deformation[i], 10 - deformation[i]), frame_avatar
Expand Down Expand Up @@ -915,7 +922,7 @@ def get_spank_frames_figures(
frame_object = Image.open(DATA_DIR / f"spank_figures/{img}.png")
frame_source_avatar = source_avatar.resize((64, 64))
frame_target_avatar = rotated_avatar.resize((64, 64))
frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))

frame.paste(
frame_target_avatar,
Expand Down Expand Up @@ -951,7 +958,7 @@ def get_lick_frames(avatar: Image.Image) -> List[Image.Image]:
frame_avatar = avatar.resize((64, 64))
frame_object = Image.open(DATA_DIR / f"lick/{img}.png")

frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame.paste(frame_object, (10, 15), frame_object)
frame.paste(frame_avatar, (198 + voffset[i], 68 + hoffset[i]), frame_avatar)
frames.append(frame)
Expand All @@ -977,7 +984,7 @@ def get_hyperlick_frames(avatar: Image.Image) -> List[Image.Image]:
)
frame_object = Image.open(DATA_DIR / f"lick/{img}.png")

frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
frame = Image.new("RGBA", (width, height), (0, 0, 0, 0))
frame.paste(frame_object, (10, 15), frame_object)
frame.paste(frame_avatar, (198 + voffset[i], 68 + hoffset[i]), frame_avatar)
frames.append(frame)
Expand Down

0 comments on commit ad577ba

Please sign in to comment.