長文の折り返しに対応
This commit is contained in:
@@ -139,7 +139,7 @@ class OverlayImage:
|
|||||||
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:
|
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.
|
"""Render a single textbox (original message) with per-token centered ruby (romaji above hiragana) over each original token.
|
||||||
|
|
||||||
Fallback: if wrapping would occur (message too wide) or tokens mismatch, revert to block-level ruby (renderRubyBlock + createTextboxSmallLog).
|
When wrapping occurs, splits tokens into lines and renders ruby above each line separately.
|
||||||
"""
|
"""
|
||||||
if not message or not transliteration:
|
if not message or not transliteration:
|
||||||
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
|
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
|
||||||
@@ -150,59 +150,66 @@ class OverlayImage:
|
|||||||
ruby_size = max(1, int(font_size * ruby_font_scale))
|
ruby_size = max(1, int(font_size * ruby_font_scale))
|
||||||
font_ruby = self._get_font(font_family, ruby_size)
|
font_ruby = self._get_font(font_family, ruby_size)
|
||||||
|
|
||||||
# Token width measurement
|
# Prepare temporary draw for measuring widths
|
||||||
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
||||||
draw_tmp = ImageDraw.Draw(draw_tmp_img)
|
draw_tmp = ImageDraw.Draw(draw_tmp_img)
|
||||||
|
|
||||||
|
# Build token_infos with measured widths
|
||||||
token_infos = []
|
token_infos = []
|
||||||
total_width = 0
|
|
||||||
for tok in transliteration:
|
for tok in transliteration:
|
||||||
orig = tok.get("orig", "")
|
orig = tok.get("orig", "")
|
||||||
if not orig:
|
if not orig:
|
||||||
continue
|
continue
|
||||||
hira = tok.get("hira", "")
|
hira = tok.get("hira", "")
|
||||||
romaji = tok.get("hepburn", "")
|
romaji = tok.get("hepburn", "")
|
||||||
|
try:
|
||||||
orig_w = max(1, int(draw_tmp.textlength(orig, font_orig)))
|
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
|
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
|
romaji_w = max(0, int(draw_tmp.textlength(romaji, font_ruby))) if romaji else 0
|
||||||
layout_w = max(orig_w, hira_w, romaji_w) # allocate width so ruby lines never overflow neighboring token
|
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))
|
token_infos.append((orig, hira, romaji, layout_w))
|
||||||
total_width += layout_w
|
|
||||||
|
|
||||||
if not token_infos:
|
if not token_infos:
|
||||||
# Fallback
|
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
|
||||||
ruby_block = self.renderRubyBlock(transliteration, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
|
|
||||||
base_img = self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
|
|
||||||
if ruby_block:
|
|
||||||
return self.concatenateImagesVertically(ruby_block, base_img)
|
|
||||||
return base_img
|
|
||||||
|
|
||||||
# Simple wrapping detection: if total width exceeds base_width * 0.9 → fallback
|
# Split tokens into lines based on base_width * 0.9
|
||||||
if total_width > base_width * 0.9:
|
max_line_width = base_width * 0.9
|
||||||
ruby_block = self.renderRubyBlock(transliteration, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
|
lines = []
|
||||||
base_img = self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
|
current_line = []
|
||||||
if ruby_block:
|
current_line_width = 0
|
||||||
return self.concatenateImagesVertically(ruby_block, base_img)
|
for tok_info in token_infos:
|
||||||
return base_img
|
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)
|
||||||
|
|
||||||
# Compute left start for centering complete line
|
# Render each line with per-token ruby
|
||||||
start_x = (base_width - total_width) // 2
|
outer_padding = 10
|
||||||
# Vertical positioning
|
line_images = []
|
||||||
# Symmetric outer padding: make top padding equal to bottom padding (previously top was 4, bottom ~10)
|
for line_tokens in lines:
|
||||||
outer_padding = 10 # uniform top & bottom padding for visual balance
|
line_width = sum(t[3] for t in line_tokens)
|
||||||
ruby_lines_count = 0
|
start_x = (base_width - line_width) // 2
|
||||||
has_romaji_any = any(r for (_, _, r, _) in token_infos)
|
|
||||||
has_hira_any = any(h for (_, h, _, _) in token_infos)
|
has_romaji_any = any(r for (_, _, r, _) in line_tokens)
|
||||||
if has_romaji_any:
|
has_hira_any = any(h for (_, h, _, _) in line_tokens)
|
||||||
ruby_lines_count += 1
|
ruby_lines_count = (1 if has_romaji_any else 0) + (1 if has_hira_any else 0)
|
||||||
if has_hira_any:
|
|
||||||
ruby_lines_count += 1
|
|
||||||
# Height calculation (replace asymmetric 4/10 with symmetric outer_padding)
|
|
||||||
ruby_block_height = ruby_lines_count * ruby_size + (ruby_line_spacing if ruby_lines_count == 2 else 0)
|
ruby_block_height = ruby_lines_count * ruby_size + (ruby_line_spacing if ruby_lines_count == 2 else 0)
|
||||||
total_height = outer_padding + ruby_block_height + ruby_original_spacing + font_size + outer_padding
|
line_height = outer_padding + ruby_block_height + ruby_original_spacing + font_size + outer_padding
|
||||||
img = Image.new("RGBA", (base_width, total_height), (0, 0, 0, 0))
|
|
||||||
draw = ImageDraw.Draw(img)
|
line_img = Image.new("RGBA", (base_width, line_height), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(line_img)
|
||||||
|
|
||||||
# Y centers
|
|
||||||
current_y = outer_padding + ruby_size // 2
|
current_y = outer_padding + ruby_size // 2
|
||||||
romaji_y = current_y if has_romaji_any else None
|
romaji_y = current_y if has_romaji_any else None
|
||||||
hira_y = None
|
hira_y = None
|
||||||
@@ -213,9 +220,8 @@ class OverlayImage:
|
|||||||
|
|
||||||
orig_y = outer_padding + ruby_block_height + ruby_original_spacing + font_size // 2
|
orig_y = outer_padding + ruby_block_height + ruby_original_spacing + font_size // 2
|
||||||
|
|
||||||
# Draw tokens sequentially
|
|
||||||
cursor_x = start_x
|
cursor_x = start_x
|
||||||
for orig, hira, romaji, w in token_infos:
|
for orig, hira, romaji, w in line_tokens:
|
||||||
token_center_x = cursor_x + w // 2
|
token_center_x = cursor_x + w // 2
|
||||||
if romaji_y is not None and romaji:
|
if romaji_y is not None and romaji:
|
||||||
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
|
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
|
||||||
@@ -223,7 +229,17 @@ class OverlayImage:
|
|||||||
draw.text((token_center_x, hira_y), hira, text_color, anchor="mm", font=font_ruby)
|
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)
|
draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig)
|
||||||
cursor_x += w
|
cursor_x += w
|
||||||
return img
|
|
||||||
|
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:
|
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設定を取得
|
||||||
@@ -363,7 +379,7 @@ class OverlayImage:
|
|||||||
def createTextboxLargeLogWithRubyTokens(self, message_type: str, size: str, message: str, transliteration: List[dict], language: str, ruby_font_scale: float, ruby_line_spacing: int) -> Image:
|
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.
|
"""Render a large-log textbox with per-token centered ruby above each original token.
|
||||||
|
|
||||||
Falls back to block-level ruby if message wraps or tokens mismatch.
|
When wrapping occurs, splits tokens into lines and renders ruby above each line separately.
|
||||||
"""
|
"""
|
||||||
ui_size = self.getUiSizeLargeLog()
|
ui_size = self.getUiSizeLargeLog()
|
||||||
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
|
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
|
||||||
@@ -377,55 +393,66 @@ class OverlayImage:
|
|||||||
if not message or not transliteration:
|
if not message or not transliteration:
|
||||||
return self.createTextImageLargeLog(message_type, size, message, language)
|
return self.createTextImageLargeLog(message_type, size, message, language)
|
||||||
|
|
||||||
# Reject multiline for per-token layout; fallback to block ruby
|
|
||||||
if "\n" in message:
|
|
||||||
ruby_block = self.renderRubyBlock(transliteration, language, ui_size["width"], font_size, ruby_font_scale, ruby_line_spacing, text_color)
|
|
||||||
base_img = self.createTextImageLargeLog(message_type, size, message, language)
|
|
||||||
if ruby_block is not None:
|
|
||||||
return self.concatenateImagesVertically(ruby_block, base_img)
|
|
||||||
return base_img
|
|
||||||
|
|
||||||
# Measure token widths
|
# Measure token widths
|
||||||
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
||||||
draw_tmp = ImageDraw.Draw(draw_tmp_img)
|
draw_tmp = ImageDraw.Draw(draw_tmp_img)
|
||||||
token_infos = []
|
token_infos = []
|
||||||
total_width = 0
|
|
||||||
for tok in transliteration:
|
for tok in transliteration:
|
||||||
orig = tok.get("orig", "")
|
orig = tok.get("orig", "")
|
||||||
if not orig:
|
if not orig:
|
||||||
continue
|
continue
|
||||||
hira = tok.get("hira", "")
|
hira = tok.get("hira", "")
|
||||||
romaji = tok.get("hepburn", "")
|
romaji = tok.get("hepburn", "")
|
||||||
|
try:
|
||||||
orig_w = max(1, int(draw_tmp.textlength(orig, font_orig)))
|
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
|
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
|
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)
|
layout_w = max(orig_w, hira_w, romaji_w)
|
||||||
token_infos.append((orig, hira, romaji, layout_w))
|
token_infos.append((orig, hira, romaji, layout_w))
|
||||||
total_width += layout_w
|
|
||||||
|
|
||||||
# Fallback if nothing to render or would overflow
|
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"]
|
base_width = ui_size["width"]
|
||||||
if not token_infos or total_width > base_width * 0.9:
|
max_line_width = base_width * 0.9
|
||||||
ruby_block = self.renderRubyBlock(transliteration, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
|
lines = []
|
||||||
base_img = self.createTextImageLargeLog(message_type, size, message, language)
|
current_line = []
|
||||||
if ruby_block is not None:
|
current_line_width = 0
|
||||||
return self.concatenateImagesVertically(ruby_block, base_img)
|
for tok_info in token_infos:
|
||||||
return base_img
|
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)
|
||||||
|
|
||||||
# Determine start_x according to message type (left for receive, right-align for send)
|
# Render each line with per-token ruby
|
||||||
start_x = 0 if message_type == "receive" else (base_width - total_width)
|
|
||||||
|
|
||||||
# Vertical layout
|
|
||||||
outer_padding = 10
|
outer_padding = 10
|
||||||
has_romaji_any = any(r for (_, _, r, _) in token_infos)
|
line_images = []
|
||||||
has_hira_any = any(h for (_, h, _, _) in token_infos)
|
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_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)
|
ruby_block_height = ruby_lines_count * ruby_size + (ruby_line_spacing if ruby_lines_count == 2 else 0)
|
||||||
total_height = outer_padding + ruby_block_height + font_size + outer_padding
|
line_height = outer_padding + ruby_block_height + font_size + outer_padding
|
||||||
img = Image.new("RGBA", (base_width, total_height), (0, 0, 0, 0))
|
|
||||||
draw = ImageDraw.Draw(img)
|
line_img = Image.new("RGBA", (base_width, line_height), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(line_img)
|
||||||
|
|
||||||
# y positions
|
|
||||||
current_y = outer_padding + ruby_size // 2
|
current_y = outer_padding + ruby_size // 2
|
||||||
romaji_y = current_y if has_romaji_any else None
|
romaji_y = current_y if has_romaji_any else None
|
||||||
hira_y = None
|
hira_y = None
|
||||||
@@ -433,11 +460,11 @@ class OverlayImage:
|
|||||||
hira_y = romaji_y + ruby_size + ruby_line_spacing
|
hira_y = romaji_y + ruby_size + ruby_line_spacing
|
||||||
elif has_hira_any:
|
elif has_hira_any:
|
||||||
hira_y = current_y
|
hira_y = current_y
|
||||||
|
|
||||||
orig_y = outer_padding + ruby_block_height + font_size // 2
|
orig_y = outer_padding + ruby_block_height + font_size // 2
|
||||||
|
|
||||||
# Draw tokens
|
|
||||||
cursor_x = start_x
|
cursor_x = start_x
|
||||||
for orig, hira, romaji, w in token_infos:
|
for orig, hira, romaji, w in line_tokens:
|
||||||
token_center_x = cursor_x + w // 2
|
token_center_x = cursor_x + w // 2
|
||||||
if romaji_y is not None and romaji:
|
if romaji_y is not None and romaji:
|
||||||
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
|
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
|
||||||
@@ -445,7 +472,17 @@ class OverlayImage:
|
|||||||
draw.text((token_center_x, hira_y), hira, text_color, anchor="mm", font=font_ruby)
|
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)
|
draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig)
|
||||||
cursor_x += w
|
cursor_x += w
|
||||||
return img
|
|
||||||
|
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:
|
def createTextImageMessageType(self, message_type: str, date_time: str) -> Image:
|
||||||
ui_size = self.getUiSizeLargeLog()
|
ui_size = self.getUiSizeLargeLog()
|
||||||
@@ -665,3 +702,32 @@ if __name__ == "__main__":
|
|||||||
]
|
]
|
||||||
])
|
])
|
||||||
img.save("overlay_large.png")
|
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.")
|
||||||
Reference in New Issue
Block a user