diff --git a/src/assets/fonts/DejaVuSans-Bold-LICENSE.txt b/src/assets/fonts/DejaVuSans-Bold-LICENSE.txt new file mode 100644 index 0000000..ce931a0 --- /dev/null +++ b/src/assets/fonts/DejaVuSans-Bold-LICENSE.txt @@ -0,0 +1,51 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: DejaVu fonts +Upstream-Author: Stepan Roh (original author), + see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list +Source: https://dejavu-fonts.github.io/ + +Files: * +Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. + Bitstream Vera is a trademark of Bitstream, Inc. + DejaVu changes are in public domain. +License: bitstream-vera + Permission is hereby granted, free of charge, to any person obtaining a copy + of the fonts accompanying this license ("Fonts") and associated + documentation files (the "Font Software"), to reproduce and distribute the + Font Software, including without limitation the rights to use, copy, merge, + publish, distribute, and/or sell copies of the Font Software, and to permit + persons to whom the Font Software is furnished to do so, subject to the + following conditions: + . + The above copyright and trademark notices and this permission notice shall + be included in all copies of one or more of the Font Software typefaces. + . + The Font Software may be modified, altered, or added to, and in particular + the designs of glyphs or characters in the Fonts may be modified and + additional glyphs or characters may be added to the Fonts, only if the fonts + are renamed to names not containing either the words "Bitstream" or the word + "Vera". + . + This License becomes null and void to the extent applicable to Fonts or Font + Software that has been modified and is distributed under the "Bitstream + Vera" names. + . + The Font Software may be sold as part of a larger software package but no + copy of one or more of the Font Software typefaces may be sold by itself. + . + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, + TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME + FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING + ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE + FONT SOFTWARE. + . + Except as contained in this notice, the names of Gnome, the Gnome + Foundation, and Bitstream Inc., shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Font Software + without prior written authorization from the Gnome Foundation or Bitstream + Inc., respectively. For further information, contact: fonts at gnome dot + org. diff --git a/src/assets/fonts/DejaVuSans-Bold.ttf b/src/assets/fonts/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..0a7fb3f Binary files /dev/null and b/src/assets/fonts/DejaVuSans-Bold.ttf differ diff --git a/src/cogs/meme.py b/src/cogs/meme.py index 17c61ad..e5c4380 100644 --- a/src/cogs/meme.py +++ b/src/cogs/meme.py @@ -12,11 +12,13 @@ import yt_dlp from discord import app_commands from discord.ext import commands from moviepy import CompositeVideoClip -from moviepy import TextClip +from moviepy import ImageClip from moviepy import VideoFileClip from moviepy import vfx from PIL import Image +from PIL import ImageDraw from PIL import ImageEnhance +from PIL import ImageFont from PIL import UnidentifiedImageError from cogs.utils import discord_utils @@ -27,6 +29,7 @@ NIGHTCORE_SPEED = 1.33 # Resample first so the speed-up is the same whatever the source's sample rate is NIGHTCORE_FILTER = f"aresample=44100,asetrate=44100*{NIGHTCORE_SPEED},aresample=44100,bass=g=6:f=100:w=0.5,volume=0.8" MP3_BITRATES = [192, 160, 128, 112, 96, 80, 64] +CRAB_FONT = "./src/assets/fonts/DejaVuSans-Bold.ttf" class Meme(commands.Cog): @@ -382,6 +385,40 @@ class Meme(commands.Cog): except (FileNotFoundError, PermissionError): self.bot.logger.warning(f"Failed to remove temporary file {temp_file}") + @staticmethod + def text_clip(text: str, font_size: int, stroke_width: int = 0) -> ImageClip: + """ + Render white text with a black outline as a transparent clip. + moviepy's own TextClip cuts off everything below the tallest letters with Pillow 11.2 and newer, + so the image is made as tall as the font's ascent and descent instead + + Parameters + ---------- + text (str): The text to render + font_size (int): The font size + stroke_width (int): The width of the black outline + + Returns + ---------- + (ImageClip): The text with a transparent background + """ + + font = ImageFont.truetype(CRAB_FONT, font_size) + ascent, descent = font.getmetrics() + left, _, right, _ = font.getbbox(text, stroke_width=stroke_width, anchor="ls") + + image = Image.new("RGBA", (right - left, ascent + descent + 2 * stroke_width), (0, 0, 0, 0)) + ImageDraw.Draw(image).text( + (-left, ascent + stroke_width), + text, + font=font, + fill="white", + anchor="ls", + stroke_width=stroke_width, + stroke_fill="black", + ) + return ImageClip(np.array(image)) + @staticmethod def make_crab(top_text: str, bottom_text: str, user_id: int): """ @@ -397,41 +434,16 @@ class Meme(commands.Cog): clip = VideoFileClip("./src/assets/crab.mp4") - top_part = ( - TextClip(text=top_text, font_size=60, color="white", stroke_width=2, stroke_color="black") - .with_start(11.0) - .with_position(("center", 300)) - .with_duration(26.0) - ) - middle_part = ( - TextClip( - text="____________________", - font_size=48, - color="white", - ) - .with_start(11.0) - .with_position(("center", "center")) - .with_duration(26.0) - ) - bottom_part = ( - TextClip(text=bottom_text, font_size=60, color="white", stroke_width=2, stroke_color="black") - .with_start(11.0) - .with_position(("center", 400)) - .with_duration(26.0) - ) + top_part = Meme.text_clip(top_text, 60, stroke_width=2).with_position(("center", 300)) + middle_part = Meme.text_clip("____________________", 48).with_position(("center", "center")) + bottom_part = Meme.text_clip(bottom_text, 60, stroke_width=2).with_position(("center", 400)) - top_part = top_part.with_effects([vfx.CrossFadeIn(1)]) - middle_part = middle_part.with_effects([vfx.CrossFadeIn(1)]) - bottom_part = bottom_part.with_effects([vfx.CrossFadeIn(1)]) + parts = [ + part.with_start(11.0).with_duration(26.0).with_effects([vfx.CrossFadeIn(1)]) + for part in (top_part, middle_part, bottom_part) + ] - video = CompositeVideoClip( - [ - clip, - top_part, - middle_part, - bottom_part, - ] - ).with_duration(26.0) + video = CompositeVideoClip([clip, *parts]).with_duration(26.0) video.write_videofile( f"./src/assets/temp/{user_id}_crab.mp4",