Files
VRCT/src-python/models/overlay/overlay_image.py
2025-10-23 16:23:50 +09:00

733 lines
36 KiB
Python

from os import path as os_path
from datetime import datetime
from typing import Tuple, List, Optional
from PIL import Image, ImageDraw, ImageFont
try:
from utils import errorLogging
except ImportError:
def errorLogging():
import traceback
print(traceback.format_exc())
class OverlayImage:
LANGUAGES = {
"Default": "NotoSansJP-Regular.ttf",
"Japanese": "NotoSansJP-Regular.ttf",
"Korean": "NotoSansKR-Regular.ttf",
"Chinese Simplified": "NotoSansSC-Regular.ttf",
"Chinese Traditional": "NotoSansTC-Regular.ttf",
}
def __init__(self, root_path: Optional[str] = None) -> None:
"""Overlay image helper.
Args:
root_path: optional project root to resolve bundled fonts. If omitted,
defaults to repository `fonts` directory.
"""
self.message_log: List[dict] = []
# PyInstallerでビルドされた場合のパス
if root_path and os_path.exists(os_path.join(root_path, "_internal", "fonts")):
self.root_path = os_path.join(root_path, "_internal", "fonts")
# src-pythonフォルダから直接実行している場合のパス
elif os_path.exists(os_path.join(os_path.dirname(__file__), "models", "overlay", "fonts")):
self.root_path = os_path.join(os_path.dirname(__file__), "models", "overlay", "fonts")
# overlayフォルダから直接実行している場合のパス
elif os_path.exists(os_path.join(os_path.dirname(__file__), "fonts")):
self.root_path = os_path.join(os_path.dirname(__file__), "fonts")
else:
raise FileNotFoundError("Font directory not found.")
# Simple in-memory font cache to avoid repeated truetype loading cost.
self._font_cache = {}
@staticmethod
def concatenateImagesVertically(img1: Image, img2: Image, margin: int = 0) -> Image:
total_height = img1.height + img2.height + margin
dst = Image.new("RGBA", (img1.width, total_height))
dst.paste(img1, (0, 0))
dst.paste(img2, (0, img1.height + margin))
return dst
@staticmethod
def addImageMargin(image: Image, top: int, right: int, bottom: int, left: int, color: Tuple[int, int, int, int]) -> Image:
new_width = image.width + right + left
new_height = image.height + top + bottom
result = Image.new(image.mode, (new_width, new_height), color)
result.paste(image, (left, top))
return result
@staticmethod
def getUiSizeSmallLog() -> dict:
return {
"width": 3840,
"height": 92,
"font_size": 92,
}
@staticmethod
def getUiColorSmallLog() -> dict:
colors = {
"background_color": (41, 42, 45),
"background_outline_color": (41, 42, 45),
"text_color": (223, 223, 223)
}
return colors
def _get_font(self, font_family: str, size: int) -> ImageFont.FreeTypeFont:
font_path = os_path.join(self.root_path, font_family)
key = (font_path, size)
if key not in self._font_cache:
self._font_cache[key] = ImageFont.truetype(font_path, size)
return self._font_cache[key]
def createTextboxSmallLog(self, text: str, language: str, text_color: Tuple[int, int, int], base_width: int, base_height: int, font_size: int) -> Image:
if text is None:
text = ""
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
font = self._get_font(font_family, font_size)
# Initial image for width measurement
img_tmp = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0))
draw_tmp = ImageDraw.Draw(img_tmp)
try:
text_width = draw_tmp.textlength(text, font) if len(text) > 0 else 1
character_width = max(1, text_width // max(1, len(text)))
character_line_num = int((base_width // character_width) - 12)
if len(text) > character_line_num and character_line_num > 0:
text = "\n".join([text[i:i + character_line_num] for i in range(0, len(text), character_line_num)])
except Exception:
errorLogging()
lines = text.split("\n") if text else [""]
text_height = font_size * (len(lines) + 1) + 20
img = Image.new("RGBA", (base_width, text_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
text_x = base_width // 2
text_y = text_height // 2
draw.text((text_x, text_y), text, text_color, anchor="mm", stroke_width=0, font=font, align="center")
return img
def renderRubyBlock(self, transliteration: List[dict], language: str, base_width: int, base_font_size: int, ruby_font_scale: float, ruby_line_spacing: int, text_color: Tuple[int, int, int]) -> Optional[Image.Image]:
# Build romaji and hiragana lines.
romaji_line = " ".join([t.get("hepburn", "") for t in transliteration if t.get("hepburn")])
hira_line = " ".join([t.get("hira", "") for t in transliteration if t.get("hira")])
if not romaji_line and not hira_line:
return None
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
ruby_size = max(1, int(base_font_size * ruby_font_scale))
font_ruby = self._get_font(font_family, ruby_size)
# Symmetric outer padding so ruby block has breathing room top/bottom
outer_padding = 10
# Measure widths to center lines independently.
img_tmp = Image.new("RGBA", (base_width, ruby_size * 2 + ruby_line_spacing + outer_padding * 2), (0, 0, 0, 0))
draw_tmp = ImageDraw.Draw(img_tmp)
romaji_width = draw_tmp.textlength(romaji_line, font_ruby) if romaji_line else 0
hira_width = draw_tmp.textlength(hira_line, font_ruby) if hira_line else 0
romaji_x = (base_width - romaji_width) // 2
hira_x = (base_width - hira_width) // 2
# Construct final ruby image with symmetric padding
ruby_height = outer_padding + ruby_size * (2 if hira_line and romaji_line else 1) + (ruby_line_spacing if hira_line and romaji_line else 0) + outer_padding
ruby_img = Image.new("RGBA", (base_width, ruby_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(ruby_img)
current_y = outer_padding + ruby_size // 2
if romaji_line:
draw.text((romaji_x + romaji_width // 2, current_y), romaji_line, text_color, anchor="mm", font=font_ruby)
current_y += ruby_size + (ruby_line_spacing if hira_line else 0)
if hira_line:
draw.text((hira_x + hira_width // 2, current_y), hira_line, text_color, anchor="mm", font=font_ruby)
return ruby_img
def createTextboxSmallLogWithRubyTokens(self, message: str, transliteration: List[dict], language: str, text_color: Tuple[int, int, int], base_width: int, font_size: int, ruby_font_scale: float, ruby_line_spacing: int, ruby_original_spacing: int) -> Image:
"""Render a single textbox (original message) with per-token centered ruby (romaji above hiragana) over each original token.
When wrapping occurs, splits tokens into lines and renders ruby above each line separately.
"""
if not message or not transliteration:
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
# Obtain font instances
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
font_orig = self._get_font(font_family, font_size)
ruby_size = max(1, int(font_size * ruby_font_scale))
font_ruby = self._get_font(font_family, ruby_size)
# Prepare temporary draw for measuring widths
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
draw_tmp = ImageDraw.Draw(draw_tmp_img)
# Build token_infos with measured widths
token_infos = []
for tok in transliteration:
orig = tok.get("orig", "")
if not orig:
continue
hira = tok.get("hira", "")
romaji = tok.get("hepburn", "")
try:
orig_w = max(1, int(draw_tmp.textlength(orig, font_orig)))
hira_w = max(0, int(draw_tmp.textlength(hira, font_ruby))) if hira else 0
romaji_w = max(0, int(draw_tmp.textlength(romaji, font_ruby))) if romaji else 0
except Exception:
errorLogging()
orig_w = len(orig) * font_size // 2
hira_w = len(hira) * ruby_size // 2 if hira else 0
romaji_w = len(romaji) * ruby_size // 2 if romaji else 0
layout_w = max(orig_w, hira_w, romaji_w)
token_infos.append((orig, hira, romaji, layout_w))
if not token_infos:
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
# Split tokens into lines based on base_width * 0.9
max_line_width = base_width * 0.9
lines = []
current_line = []
current_line_width = 0
for tok_info in token_infos:
tok_width = tok_info[3]
if current_line_width + tok_width > max_line_width and current_line:
lines.append(current_line)
current_line = [tok_info]
current_line_width = tok_width
else:
current_line.append(tok_info)
current_line_width += tok_width
if current_line:
lines.append(current_line)
# Render each line with per-token ruby
outer_padding = 10
line_images = []
for line_tokens in lines:
line_width = sum(t[3] for t in line_tokens)
start_x = (base_width - line_width) // 2
has_romaji_any = any(r for (_, _, r, _) in line_tokens)
has_hira_any = any(h for (_, h, _, _) in line_tokens)
ruby_lines_count = (1 if has_romaji_any else 0) + (1 if has_hira_any else 0)
ruby_block_height = ruby_lines_count * ruby_size + (ruby_line_spacing if ruby_lines_count == 2 else 0)
line_height = outer_padding + ruby_block_height + ruby_original_spacing + font_size + outer_padding
line_img = Image.new("RGBA", (base_width, line_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(line_img)
current_y = outer_padding + ruby_size // 2
romaji_y = current_y if has_romaji_any else None
hira_y = None
if has_romaji_any and has_hira_any:
hira_y = romaji_y + ruby_size + ruby_line_spacing
elif has_hira_any:
hira_y = current_y
orig_y = outer_padding + ruby_block_height + ruby_original_spacing + font_size // 2
cursor_x = start_x
for orig, hira, romaji, w in line_tokens:
token_center_x = cursor_x + w // 2
if romaji_y is not None and romaji:
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
if hira_y is not None and hira:
draw.text((token_center_x, hira_y), hira, text_color, anchor="mm", font=font_ruby)
draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig)
cursor_x += w
line_images.append(line_img)
# Concatenate all lines vertically
if not line_images:
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
result_img = line_images[0]
for line_img in line_images[1:]:
result_img = self.concatenateImagesVertically(result_img, line_img, margin=0)
return result_img
def createOverlayImageSmallLog(self, message: str, your_language: str, translation: List[str] = [], target_language: List[str] = [], transliteration_message: List[dict] = [], transliteration_translation: List[List[dict]] = [], ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
# UI設定を取得
ui_size = self.getUiSizeSmallLog()
width, height, font_size = ui_size["width"], ui_size["height"], ui_size["font_size"]
ui_colors = self.getUiColorSmallLog()
text_color = ui_colors["text_color"]
background_color = ui_colors["background_color"]
background_outline_color = ui_colors["background_outline_color"]
# テキストボックス画像のリストを作成
textbox_images = []
# 翻訳がある場合
# Use improved per-token placement if possible; else fallback to previous block approach.
ruby_original_spacing = 2 # Narrow vertical gap between hiragana block and original text.
if translation and target_language:
if message:
base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
textbox_images.append(base_msg_img)
for trans, lang, translite in zip(translation, target_language, transliteration_translation):
try:
trans_img = self.createTextboxSmallLogWithRubyTokens(
trans,
translite,
lang,
text_color,
width,
font_size,
ruby_font_scale,
ruby_line_spacing,
ruby_original_spacing,
)
except Exception:
errorLogging()
trans_img = self.createTextboxSmallLog(trans, lang, text_color, width, height, font_size)
textbox_images.append(trans_img)
else:
# 翻訳無しモード
if message and transliteration_message:
try:
base_msg_img = self.createTextboxSmallLogWithRubyTokens(
message,
transliteration_message,
your_language,
text_color,
width,
font_size,
ruby_font_scale,
ruby_line_spacing,
ruby_original_spacing,
)
except Exception:
errorLogging()
base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
try:
ruby_img = self.renderRubyBlock(transliteration_message, your_language, width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
if ruby_img is not None:
base_msg_img = self.concatenateImagesVertically(ruby_img, base_msg_img)
except Exception:
errorLogging()
else:
base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
textbox_images.append(base_msg_img)
# すべてのテキストボックスを縦に結合
img = textbox_images[0]
for textbox_img in textbox_images[1:]:
img = self.concatenateImagesVertically(img, textbox_img)
# 画像周囲にUIパディングを追加して、文字が端に張り付かないようにする
ui_outer_padding = 50
img = self.addImageMargin(img, ui_outer_padding, ui_outer_padding, ui_outer_padding, ui_outer_padding, (0, 0, 0, 0))
# 角丸背景を作成
background = Image.new("RGBA", img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(background)
draw.rounded_rectangle([(0, 0), img.size], radius=50, fill=background_color, outline=background_outline_color, width=5)
# 背景とテキストを合成
img = Image.alpha_composite(background, img)
return img
@staticmethod
def getUiSizeLargeLog() -> dict:
return {
"width": 960,
"font_size_large": 30,
"font_size_small": 20,
"margin": 25,
"radius": 25,
"padding": 10,
"clause_margin": 20,
}
@staticmethod
def getUiColorLargeLog() -> dict:
return {
"background_color": (41, 42, 45),
"background_outline_color": (41, 42, 45),
"text_color_large": (223, 223, 223),
"text_color_small": (190, 190, 190),
"text_color_send": (97, 151, 180),
"text_color_receive": (168, 97, 180),
"text_color_time": (120, 120, 120)
}
def createTextImageLargeLog(self, message_type: str, size: str, text: str, language: str) -> Image:
ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
text_color = self.getUiColorLargeLog()[f"text_color_{size}"]
anchor = "lm" if message_type == "receive" else "rm"
text_x = 0 if message_type == "receive" else ui_size["width"]
align = "left" if message_type == "receive" else "right"
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
img = Image.new("RGBA", (0, 0), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
font_path = os_path.join(self.root_path, font_family)
font = ImageFont.truetype(font_path, font_size)
# 改行を含んだtextの最大の文字数を計算する
text_width = max(draw.textlength(line, font) for line in text.split("\n"))
character_width = text_width // len(text)
character_line_num = int((ui_size["width"] // character_width) - 1)
if len(text) > character_line_num:
text = "\n".join([text[i:i + character_line_num] for i in range(0, len(text), character_line_num)])
text_height = font_size * len(text.split("\n")) + ui_size["padding"]
img = Image.new("RGBA", (ui_size["width"], text_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
text_y = text_height // 2
draw.multiline_text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font, align=align)
return img
def createTextboxLargeLogWithRubyTokens(self, message_type: str, size: str, message: str, transliteration: List[dict], language: str, ruby_font_scale: float, ruby_line_spacing: int) -> Image:
"""Render a large-log textbox with per-token centered ruby above each original token.
When wrapping occurs, splits tokens into lines and renders ruby above each line separately.
"""
ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
text_color = self.getUiColorLargeLog()[f"text_color_{size}"]
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
font_orig = self._get_font(font_family, font_size)
ruby_size = max(1, int(font_size * ruby_font_scale))
font_ruby = self._get_font(font_family, ruby_size)
# Simple guard
if not message or not transliteration:
return self.createTextImageLargeLog(message_type, size, message, language)
# Measure token widths
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
draw_tmp = ImageDraw.Draw(draw_tmp_img)
token_infos = []
for tok in transliteration:
orig = tok.get("orig", "")
if not orig:
continue
hira = tok.get("hira", "")
romaji = tok.get("hepburn", "")
try:
orig_w = max(1, int(draw_tmp.textlength(orig, font_orig)))
hira_w = max(0, int(draw_tmp.textlength(hira, font_ruby))) if hira else 0
romaji_w = max(0, int(draw_tmp.textlength(romaji, font_ruby))) if romaji else 0
except Exception:
errorLogging()
orig_w = len(orig) * font_size // 2
hira_w = len(hira) * ruby_size // 2 if hira else 0
romaji_w = len(romaji) * ruby_size // 2 if romaji else 0
layout_w = max(orig_w, hira_w, romaji_w)
token_infos.append((orig, hira, romaji, layout_w))
if not token_infos:
return self.createTextImageLargeLog(message_type, size, message, language)
# Split tokens into lines based on base_width * 0.9
base_width = ui_size["width"]
max_line_width = base_width * 0.9
lines = []
current_line = []
current_line_width = 0
for tok_info in token_infos:
tok_width = tok_info[3]
if current_line_width + tok_width > max_line_width and current_line:
lines.append(current_line)
current_line = [tok_info]
current_line_width = tok_width
else:
current_line.append(tok_info)
current_line_width += tok_width
if current_line:
lines.append(current_line)
# Render each line with per-token ruby
outer_padding = 10
line_images = []
for line_tokens in lines:
line_width = sum(t[3] for t in line_tokens)
# Determine start_x according to message type (left for receive, right-align for send)
start_x = 0 if message_type == "receive" else (base_width - line_width)
has_romaji_any = any(r for (_, _, r, _) in line_tokens)
has_hira_any = any(h for (_, h, _, _) in line_tokens)
ruby_lines_count = (1 if has_romaji_any else 0) + (1 if has_hira_any else 0)
ruby_block_height = ruby_lines_count * ruby_size + (ruby_line_spacing if ruby_lines_count == 2 else 0)
line_height = outer_padding + ruby_block_height + font_size + outer_padding
line_img = Image.new("RGBA", (base_width, line_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(line_img)
current_y = outer_padding + ruby_size // 2
romaji_y = current_y if has_romaji_any else None
hira_y = None
if has_romaji_any and has_hira_any:
hira_y = romaji_y + ruby_size + ruby_line_spacing
elif has_hira_any:
hira_y = current_y
orig_y = outer_padding + ruby_block_height + font_size // 2
cursor_x = start_x
for orig, hira, romaji, w in line_tokens:
token_center_x = cursor_x + w // 2
if romaji_y is not None and romaji:
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
if hira_y is not None and hira:
draw.text((token_center_x, hira_y), hira, text_color, anchor="mm", font=font_ruby)
draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig)
cursor_x += w
line_images.append(line_img)
# Concatenate all lines vertically
if not line_images:
return self.createTextImageLargeLog(message_type, size, message, language)
result_img = line_images[0]
for line_img in line_images[1:]:
result_img = self.concatenateImagesVertically(result_img, line_img, margin=0)
return result_img
def createTextImageMessageType(self, message_type: str, date_time: str) -> Image:
ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_small"]
ui_padding = ui_size["padding"]
ui_color = self.getUiColorLargeLog()
text_color = ui_color[f"text_color_{message_type}"]
text_color_time = ui_color["text_color_time"]
anchor = "lm" if message_type == "receive" else "rm"
text = "Receive" if message_type == "receive" else "Send"
img = Image.new("RGBA", (0, 0), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
font_path = os_path.join(self.root_path, self.LANGUAGES["Default"])
font = ImageFont.truetype(font_path, font_size)
text_height = font_size + ui_padding
text_width = draw.textlength(date_time, font)
character_width = text_width // len(date_time)
img = Image.new("RGBA", (ui_size["width"], text_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
text_y = text_height // 2
text_time_x = 0 if message_type == "receive" else ui_size["width"] - (text_width + character_width)
text_x = (text_width + character_width) if message_type == "receive" else ui_size["width"]
draw.text((text_time_x, text_y), date_time, text_color_time, anchor=anchor, stroke_width=0, font=font)
draw.text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font)
return img
def createTextboxLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = [], date_time: Optional[str] = None, transliteration_message: Optional[List[dict]] = None, transliteration_translation: Optional[List[List[dict]]] = None, ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
# テキスト画像のリストを作成
images = [self.createTextImageMessageType(message_type, date_time)]
# 翻訳がある場合
if translation and target_language:
# 元のメッセージがある場合は小さいサイズで追加
if message is not None:
small_img = self.createTextImageLargeLog(message_type, "small", message, your_language)
images.append(small_img)
# 翻訳をすべて大きいサイズで追加
for trans, lang, translite in zip(translation, target_language, transliteration_translation):
try:
large_img = self.createTextboxLargeLogWithRubyTokens(message_type, "large", trans, translite, lang, ruby_font_scale, ruby_line_spacing)
except Exception:
errorLogging()
large_img = self.createTextImageLargeLog(message_type, "large", trans, lang)
images.append(large_img)
else:
# 翻訳がない場合は元のメッセージのみ
try:
large_img = self.createTextboxLargeLogWithRubyTokens(message_type, "large", message, transliteration_message, your_language, ruby_font_scale, ruby_line_spacing)
except Exception:
errorLogging()
large_img = self.createTextImageLargeLog(message_type, "large", message, your_language)
images.append(large_img)
# すべてのテキスト画像を縦に結合
combined_img = images[0]
for img in images[1:]:
combined_img = self.concatenateImagesVertically(combined_img, img)
return combined_img
def createOverlayImageLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = [], transliteration_message: List[dict] = [], transliteration_translation: List[List[dict]] = [], ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
ui_color = self.getUiColorLargeLog()
background_color = ui_color["background_color"]
background_outline_color = ui_color["background_outline_color"]
ui_size = self.getUiSizeLargeLog()
ui_margin = ui_size["margin"]
ui_radius = ui_size["radius"]
ui_clause_margin = ui_size["clause_margin"]
self.message_log.append({
"message_type": message_type,
"message": message,
"your_language": your_language,
"translation": translation,
"target_language": target_language,
"transliteration_message": transliteration_message,
"transliteration_translation": transliteration_translation,
"datetime": datetime.now().strftime("%H:%M")
})
if len(self.message_log) > 5:
self.message_log = self.message_log[-5:]
imgs = [
self.createTextboxLargeLog(
log["message_type"],
log["message"],
log["your_language"],
log["translation"],
log["target_language"],
log["datetime"],
transliteration_message=log.get("transliteration_message", [{}]),
transliteration_translation=log.get("transliteration_translation", [{}]),
ruby_font_scale=ruby_font_scale,
ruby_line_spacing=ruby_line_spacing,
) for log in self.message_log
]
img = imgs[0]
for i in imgs[1:]:
img = self.concatenateImagesVertically(img, i, ui_clause_margin)
img = self.addImageMargin(img, ui_margin, ui_margin, ui_margin, ui_margin, (0, 0, 0, 0))
width, height = img.size
background = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(background)
draw.rounded_rectangle([(0, 0), (width, height)], radius=ui_radius, fill=background_color, outline=background_outline_color, width=5)
img = Image.alpha_composite(background, img)
return img
if __name__ == "__main__":
overlay = OverlayImage()
# Basic small log test (with translation list form)
img = overlay.createOverlayImageSmallLog("Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"], [], [[]])
img.save("overlay_small.png")
# Ruby small log test (Japanese original with transliteration tokens)
ruby_tokens = [
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
# Ruby on original + ruby on translation example
translation_tokens = [
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
]
img_ruby = overlay.createOverlayImageSmallLog(
"慮る",
"Japanese",
["慮る", "慮る", "慮る"],
["Japanese", "Japanese", "Japanese"],
transliteration_message=ruby_tokens,
transliteration_translation=translation_tokens,
ruby_font_scale=0.5,
ruby_line_spacing=4,
)
img_ruby.save("overlay_small_ruby.png")
# Large log tests (adjusted to pass translation/target_language as lists)
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"], transliteration_message=[], transliteration_translation=[])
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
long_en = "Hello, World!"
long_jp = "こんにちは、世界!"
img = overlay.createOverlayImageLargeLog("send", long_en, "English", [long_jp], ["Japanese"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[[]])
img = overlay.createOverlayImageLargeLog("receive", long_jp, "Japanese", [long_en], ["English"], transliteration_message=[], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[[]])
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_message=[
{"orig": "こんにちは", "hira": "こんにちは", "hepburn": "konnichiha"},
{"orig": "世界", "hira": "sekai", "hepburn": "sekai"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img = overlay.createOverlayImageLargeLog("send", long_en, "English", [long_jp], ["Japanese"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img = overlay.createOverlayImageLargeLog("receive", long_jp, "Japanese", [long_en, long_en, long_en], ["English", "English", "English"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img.save("overlay_large.png")
# Additional tests to reproduce wrapping + ruby behaviour
# Long message that will force wrapping in small-log width
long_msg_jp = "これは非常に長いテキストです。表示幅を超えると折り返しが発生します。"*2
# Create simple transliteration tokens (one token per short chunk)
tokens = []
for i, part in enumerate(["これは", "非常に", "長い", "テキスト", "です", "表示幅", "", "超える", "", "折り返し", "", "発生", "します", "これは", "非常に", "長い", "テキスト", "です", "表示幅", "", "超える", "", "折り返し", "", "発生", "します"]):
tokens.append({"orig": part, "hira": part, "hepburn": "aaaaa"})
print("Generating test_overlay_long_small.png (small log, expected block ruby fallback)")
img_long_small = overlay.createOverlayImageSmallLog(long_msg_jp, "Japanese", [], [], transliteration_message=tokens, transliteration_translation=[[]], ruby_font_scale=0.5, ruby_line_spacing=4)
img_long_small.save("test_overlay_long_small.png")
print("Generating test_overlay_long_large.png (large log, already falls back for multiline)")
img_long_large = overlay.createOverlayImageLargeLog("receive", long_msg_jp, "Japanese", [], [], transliteration_message=tokens, transliteration_translation=[[]])
img_long_large.save("test_overlay_long_large.png")
print("Generating test_overlay_long_large.png (large log, already falls back for multiline)")
img_long_large = overlay.createOverlayImageLargeLog("receive", long_msg_jp, "Japanese", [], [], transliteration_message=tokens, transliteration_translation=[[]])
img_long_large.save("test_overlay_long_large.png")
print("Generating test_overlay_long_large.png (large log, already falls back for multiline)")
img_long_large = overlay.createOverlayImageLargeLog("receive", long_msg_jp, "Japanese", [], [], transliteration_message=tokens, transliteration_translation=[[]])
img_long_large.save("test_overlay_long_large.png")
print("Generating test_overlay_long_large.png (large log, already falls back for multiline)")
img_long_large = overlay.createOverlayImageLargeLog("receive", long_msg_jp, "Japanese", [long_msg_jp, long_msg_jp, long_msg_jp], ["Japanese", "Japanese", "Japanese"], transliteration_message=tokens, transliteration_translation=[tokens, tokens, tokens])
img_long_large.save("test_overlay_long_large.png")
print("Done. Check test_overlay_long_small.png and test_overlay_long_large.png for expected layout.")