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.")