mirror of
https://github.com/UiO-Gaming/mustafa.git
synced 2026-11-05 13:56:48 +01:00
make text drawing algorithm a util function
This commit is contained in:
3 files changed
+90
-162
No files matched your search
+4
-78
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from typing import override
|
||||
@@ -11,11 +10,9 @@ import discord
|
||||
import numpy as np
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
|
||||
from cogs.utils import embed_templates
|
||||
from cogs.utils import misc_utils
|
||||
from cogs.utils.discord_utils import Lobby
|
||||
from cogs.utils.discord_utils import LobbyView
|
||||
|
||||
@@ -96,77 +93,6 @@ class BingoGenerator:
|
||||
with open(f"{BINGO_PATH}/names.json") as f:
|
||||
NAMES = json.load(f)
|
||||
|
||||
@classmethod
|
||||
async def put_text_in_box(
|
||||
cls,
|
||||
image: np.ndarray,
|
||||
text: str,
|
||||
top_left: tuple,
|
||||
bottom_right: tuple,
|
||||
font_path: str,
|
||||
font_size: int = 10,
|
||||
color: tuple = (0, 0, 0),
|
||||
):
|
||||
"""
|
||||
Puts text inside a bounding box on the cv2 image with automatic text wrapping, using PIL for text rendering.
|
||||
|
||||
:param image: Source cv2 image (numpy array).
|
||||
:param text: Text string to be put.
|
||||
:param top_left: Tuple (x, y) defining the top-left corner of the bounding box.
|
||||
:param bottom_right: Tuple (x, y) defining the bottom-right corner of the bounding box.
|
||||
:param font_path: Path to a .ttf font file.
|
||||
:param font_size: Initial font size.
|
||||
:param color: Text color in RGB.
|
||||
:return: Image with text (numpy array).
|
||||
"""
|
||||
|
||||
# TODO: Dry. This algorithm is also seen in the memes cog
|
||||
|
||||
# Convert the cv2 image to a PIL image
|
||||
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
draw = ImageDraw.Draw(image_pil)
|
||||
|
||||
# Get coordinates of the bounding box
|
||||
x1, y1 = top_left
|
||||
x2, y2 = bottom_right
|
||||
|
||||
# Get the width and height of the bounding box
|
||||
# We multiply by 0.9 to give some padding
|
||||
w, h = (x2 - x1) * 0.9, (y2 - y1) * 0.9
|
||||
|
||||
# Initialize font width/height to 0 in order to run the while loops below at least once
|
||||
font_width = 0
|
||||
font_height = 0
|
||||
|
||||
# Add newlines to the text. Here we have gone with 15 characters per line
|
||||
# This is arbitrary, but seems to work well
|
||||
text = textwrap.fill(text, width=15)
|
||||
|
||||
# Increase the font size until the longest line of text does not fit in the bounding box anymore
|
||||
# We also have to check the height, since the text might be too tall for the box
|
||||
while font_width < w and font_height < h:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
longest_line = max(text.split("\n"), key=lambda x: font.getlength(x))
|
||||
font_width = font.getlength(longest_line)
|
||||
|
||||
# bbox doesn't work with multiline text, so we have to calculate the height manually
|
||||
# take the height of the bounding box, multiply by 1.5 (our assumed line dividor height)
|
||||
# after that, we multiply by the number of lines
|
||||
font_box = font.getbbox(text)
|
||||
font_height = ((font_box[3] - font_box[1]) * 1.5) * text.count("\n")
|
||||
|
||||
font_size += 1
|
||||
|
||||
# Actually draw the text
|
||||
draw.multiline_textbbox((w, h), text=text, font=font, align="center", anchor="mm")
|
||||
center_of_box = ((x1 + x2) // 2, (y1 + y2) // 2)
|
||||
draw.multiline_text(center_of_box, text, font=font, fill=color, align="center", anchor="mm")
|
||||
|
||||
# Convert back to cv2 image format and update the original image
|
||||
cv2_image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
|
||||
np.copyto(image, cv2_image)
|
||||
|
||||
@classmethod
|
||||
async def sample_cells(cls, players: list[str], bot: commands.Bot) -> dict[str, list[str]]:
|
||||
assert 0 < len(players) <= 10
|
||||
@@ -231,11 +157,11 @@ class BingoGenerator:
|
||||
for i in range(0, 5):
|
||||
for j in range(0, 5):
|
||||
width = 123
|
||||
await cls.put_text_in_box(
|
||||
await misc_utils.put_text_in_box(
|
||||
sliced,
|
||||
sample_space.pop(),
|
||||
(i * width + 1, j * width + 1),
|
||||
((i + 1) * width, (j + 1) * width),
|
||||
top_left=(i * width + 1, j * width + 1),
|
||||
bottom_right=((i + 1) * width, (j + 1) * width),
|
||||
font_path=cls.FONT_PATH,
|
||||
)
|
||||
|
||||
|
||||
+10
-84
@@ -1,22 +1,22 @@
|
||||
import functools
|
||||
import os
|
||||
import textwrap
|
||||
from io import BytesIO
|
||||
|
||||
import cv2
|
||||
import discord
|
||||
import numpy as np
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from moviepy.editor import CompositeVideoClip
|
||||
from moviepy.editor import TextClip
|
||||
from moviepy.editor import VideoFileClip
|
||||
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
|
||||
from cogs.utils import embed_templates
|
||||
from cogs.utils import misc_utils
|
||||
|
||||
|
||||
class Meme(commands.Cog):
|
||||
@@ -74,82 +74,6 @@ class Meme(commands.Cog):
|
||||
|
||||
await interaction.followup.send(file=discord.File(output, "deepfry.jpg"))
|
||||
|
||||
# TODO: Move this to utils
|
||||
@staticmethod
|
||||
def draw_text(image: Image.Image, text: str, offset: tuple = (0, 0)):
|
||||
"""
|
||||
Draws meme text on preferred meme template.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image (Image.Image): The meme template.
|
||||
text (str): The text to be drawn.
|
||||
font (ImageFont.FreeTypeFont): The font, with its size set, to be used.
|
||||
offset (tuple): The offset of the text from the top left corner of the image. Defaults to (0, 0).
|
||||
"""
|
||||
|
||||
box_size = (540, 540)
|
||||
box = Image.new("RGB", box_size, (255, 255, 255))
|
||||
draw = ImageDraw.Draw(box)
|
||||
|
||||
# Using a non-mono font may cause issues but eh
|
||||
# I don't want to ship a whole ass new font just to cover edge cases for this command
|
||||
# It's rather a skill issue on my end that I can't get things to stay within bounds
|
||||
font_path = "./src/assets/fonts/comic.ttf"
|
||||
|
||||
# Calculate initial font size
|
||||
# This is done by increasing the font size until the text is too wide
|
||||
max_width, max_height = box_size[0] * 0.9, box_size[1] * 0.9 # 90% of the box size
|
||||
|
||||
font_size = 20 # Initiale size. I consider this the smallest readable size
|
||||
font_width = 0 # Initiale width. This is just to make sure the while loop runs at least once
|
||||
while font_width < max_width:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
font_box = font.getbbox(text)
|
||||
font_width = font_box[2] - font_box[0]
|
||||
|
||||
font_size += 2
|
||||
|
||||
# Wrap the text based on our current font size
|
||||
text = textwrap.fill(text, width=font_size)
|
||||
|
||||
# We then have to calculate the font size again
|
||||
#
|
||||
# The problem is, font.getbbox() doesn't work with multiline text so it returns the size without the linebreaks
|
||||
# We can get around this by calculating the size of the longest line in the text
|
||||
# and then increasing the font size until that line is too wide
|
||||
#
|
||||
# We also have to make sure it doesn't get too tall
|
||||
longest_line = max(text.split("\n"), key=len)
|
||||
longest_line_font_box = font.getbbox(longest_line)
|
||||
font_width = longest_line_font_box[2] - longest_line_font_box[0]
|
||||
|
||||
# Estimate font height
|
||||
# This is done by multiplying the height of the original non-wrapped text with the number of lines
|
||||
# and then adding 20 to account for the line spacing.
|
||||
# This of course is dependant on font size itself, but it's good enough for now
|
||||
font_height = (font_box[3] - font_box[1] + 20) * text.count("\n")
|
||||
max_height = box_size[1] * 0.9
|
||||
|
||||
while font_width < max_width and font_height < max_height:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
longest_line_font_box = font.getbbox(longest_line)
|
||||
font_width = longest_line_font_box[2] - longest_line_font_box[0]
|
||||
|
||||
font_box = font.getbbox(text)
|
||||
font_height = (font_box[3] - font_box[1] + 20) * text.count("\n")
|
||||
|
||||
font_size += 2
|
||||
|
||||
# Draw textbox and text
|
||||
draw.multiline_textbbox(box_size, text=text, font=font, align="center", anchor="mm") # TODO: remove?
|
||||
text_box = box.size[0] // 2, box.size[1] // 2
|
||||
draw.multiline_text(text_box, text, font=font, fill=(0, 0, 0, 255), align="center", anchor="mm")
|
||||
|
||||
image.paste(box, offset)
|
||||
|
||||
@app_commands.checks.bot_has_permissions(attach_files=True)
|
||||
@app_commands.checks.cooldown(1, 5)
|
||||
@app_commands.command(name="preferansemem", description="Generer en drake-aktig mem basert på tekst")
|
||||
@@ -167,15 +91,17 @@ class Meme(commands.Cog):
|
||||
await interaction.response.defer()
|
||||
|
||||
# Fetch meme template
|
||||
template = Image.open("./src/assets/sivert_goodbad.jpg")
|
||||
template = cv2.imread("./src/assets/sivert_goodbad.jpg")
|
||||
|
||||
# Draw text
|
||||
Meme.draw_text(template, dårlig_tekst, offset=(540, 0))
|
||||
Meme.draw_text(template, bra_tekst, offset=(540, 540))
|
||||
font = "./src/assets/fonts/comic.ttf"
|
||||
await misc_utils.put_text_in_box(template, dårlig_tekst, (540, 0), (1080, 540), font)
|
||||
await misc_utils.put_text_in_box(template, bra_tekst, (540, 540), (1080, 1080), font)
|
||||
|
||||
# Save image to buffer
|
||||
output = BytesIO()
|
||||
template.save(output, format="jpeg")
|
||||
_, buffer = cv2.imencode(".jpg", template)
|
||||
output = BytesIO(buffer)
|
||||
cv2.imdecode(np.frombuffer(output.getbuffer(), np.uint8), -1)
|
||||
output.seek(0)
|
||||
|
||||
# Send image
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import datetime
|
||||
import textwrap
|
||||
from math import ceil
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
|
||||
MIDNIGHT = datetime.time(hour=19, minute=48, tzinfo=ZoneInfo("Europe/Oslo"))
|
||||
|
||||
|
||||
@@ -98,3 +105,72 @@ class Paginator:
|
||||
|
||||
self.current_page = self.total_page_count
|
||||
return self.get_page(self.current_page)
|
||||
|
||||
|
||||
async def put_text_in_box(
|
||||
image: np.ndarray,
|
||||
text: str,
|
||||
top_left: tuple,
|
||||
bottom_right: tuple,
|
||||
font_path: str = "./src/assets/fonts/impact.ttf",
|
||||
font_size: int = 10,
|
||||
color: tuple = (0, 0, 0),
|
||||
):
|
||||
"""
|
||||
Puts text inside a bounding box on the cv2 image with automatic text wrapping, using PIL for text rendering.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image (np.ndarray): Source cv2 image.
|
||||
text (str): Text to be put on the image.
|
||||
top_left (tuple): (x, y) Top-left corner of the bounding box.
|
||||
bottom_right (tuple): (x, y) Bottom-right corner of the bounding box.
|
||||
font_path (str): Path to the font file. Defaults to Impact font.
|
||||
font_size (int): Initial font size. Defaults to 10.
|
||||
color (tuple): Text color in RGB. Defaults to (0, 0, 0).
|
||||
"""
|
||||
|
||||
# Convert the cv2 image to a PIL image
|
||||
image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
draw = ImageDraw.Draw(image_pil)
|
||||
|
||||
# Get coordinates of the bounding box
|
||||
x1, y1 = top_left
|
||||
x2, y2 = bottom_right
|
||||
|
||||
# Get the width and height of the bounding box
|
||||
# We multiply by 0.9 to give some padding
|
||||
w, h = (x2 - x1) * 0.9, (y2 - y1) * 0.9
|
||||
|
||||
# Initialize font width/height to 0 in order to run the while loops below at least once
|
||||
font_width = 0
|
||||
font_height = 0
|
||||
|
||||
# Add newlines to the text. Here we have gone with 15 characters per line
|
||||
# This is arbitrary, but seems to work well
|
||||
text = textwrap.fill(text, width=15)
|
||||
|
||||
# Increase the font size until the longest line of text does not fit in the bounding box anymore
|
||||
# We also have to check the height, since the text might be too tall for the box
|
||||
while font_width < w and font_height < h:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
|
||||
longest_line = max(text.split("\n"), key=lambda x: font.getlength(x))
|
||||
font_width = font.getlength(longest_line)
|
||||
|
||||
# bbox doesn't work with multiline text, so we have to calculate the height manually
|
||||
# take the height of the bounding box, multiply by 1.5 (our assumed line dividor height)
|
||||
# after that, we multiply by the number of lines
|
||||
font_box = font.getbbox(text)
|
||||
font_height = ((font_box[3] - font_box[1]) * 1.5) * text.count("\n")
|
||||
|
||||
font_size += 1
|
||||
|
||||
# Actually draw the text
|
||||
draw.multiline_textbbox((w, h), text=text, font=font, align="center", anchor="mm")
|
||||
center_of_box = ((x1 + x2) // 2, (y1 + y2) // 2)
|
||||
draw.multiline_text(center_of_box, text, font=font, fill=color, align="center", anchor="mm")
|
||||
|
||||
# Convert back to cv2 image format and update the original image
|
||||
cv2_image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
|
||||
np.copyto(image, cv2_image)
|
||||
Reference in new issue
Block a user