manga-translator / utils /write_text_on_image.py
DenisT's picture
add: check for the text being small so it can't go below 0, also file isn't image.png but the current timestamp
08faac7
"""
This module contains a function to add text to an image with a bounding box.
"""
import unicodedata
import textwrap
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import cv2
import arabic_reshaper
from bidi.algorithm import get_display
def detect_script(text):
"""
Detect the script of the text
"""
scripts = set()
for char in text:
if char.isalpha():
name = unicodedata.name(char, "")
if "LATIN" in name:
scripts.add("Latin")
elif "ARABIC" in name:
scripts.add("Arabic")
elif "CYRILLIC" in name:
scripts.add("Cyrillic")
elif "GREEK" in name:
scripts.add("Greek")
elif "HEBREW" in name:
scripts.add("Hebrew")
elif "DEVANAGARI" in name:
scripts.add("Devanagari")
if not scripts:
return "Latin"
return list(scripts)[0]
def get_font_path(script):
if script == "Latin":
return "./fonts/NotoSans-Regular.ttf"
elif script == "Arabic":
return "./fonts/NotoNaskhArabic-Regular.ttf"
elif script == "Cyrillic":
return "./fonts/NotoSansCyrillic-Regular.ttf"
elif script == "Greek":
return "./fonts/NotoSansGreek-Regular.ttf"
else:
return "./fonts/NotoSans-Regular.ttf"
def add_text(image: np.ndarray, text: str, contour: np.ndarray):
script = detect_script(text)
font_path = get_font_path(script)
if script == "Arabic":
reshaped_text = arabic_reshaper.reshape(text)
text = get_display(reshaped_text)
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)
x, y, w, h = cv2.boundingRect(contour)
line_height = 16
font_size = 14
wrapping_ratio = 0.075
min_font_size = 6 # Minimum font size to prevent going to 0
max_iterations = 20 # Prevent infinite loops
wrap_width = max(1, int(w * wrapping_ratio))
wrapped_text = textwrap.fill(text, width=wrap_width, break_long_words=True)
font = ImageFont.truetype(font_path, size=font_size)
lines = wrapped_text.split("\n")
total_text_height = (len(lines)) * line_height
iterations = 0
while (
total_text_height > h
and font_size > min_font_size
and iterations < max_iterations
):
line_height = max(line_height - 2, min_font_size)
font_size = max(font_size - 2, min_font_size)
wrapping_ratio = min(wrapping_ratio + 0.025, 0.5)
wrap_width = max(1, int(w * wrapping_ratio))
wrapped_text = textwrap.fill(text, width=wrap_width, break_long_words=True)
font = ImageFont.truetype(font_path, size=font_size)
lines = wrapped_text.split("\n")
total_text_height = (len(lines)) * line_height
iterations += 1
# If text still doesn't fit after all adjustments, truncate it
if total_text_height > h:
max_lines = max(1, h // line_height)
lines = lines[:max_lines]
if len(lines) < len(wrapped_text.split("\n")):
# Add ellipsis to last line if text was truncated
if lines:
lines[-1] = lines[-1][: max(0, len(lines[-1]) - 3)] + "..."
# Vertical centering
actual_text_height = len(lines) * line_height
text_y = y + max(0, (h - actual_text_height) // 2)
for line in lines:
text_length = draw.textlength(line, font=font)
text_x = x + max(
0, (w - text_length) // 2
) # Ensure x coordinate is not negative
draw.text((text_x, text_y), line, font=font, fill=(0, 0, 0))
text_y += line_height
image[:, :, :] = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)