dumm-image-translator / image_editor.py
teuchi's picture
fixing the problems I didn't give a damn about
fe5a7d8 verified
Raw History Blame Contribute Delete
7.33 kB
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import os
import urllib.request
def get_background_color(image: Image.Image) -> tuple:
pixels = np.array(image.convert("RGB"))
edge_pixels = np.concatenate([
pixels[0, :],
pixels[-1, :],
pixels[:, 0],
pixels[:, -1],
])
colors, counts = np.unique(edge_pixels.reshape(-1, 3), axis=0, return_counts=True)
bg_color = colors[np.argmax(counts)]
return tuple(int(c) for c in bg_color)
def get_text_color(bg_color: tuple) -> tuple:
brightness = sum(bg_color) / 3
return (0, 0, 0) if brightness > 128 else (255, 255, 255)
def find_system_font() -> str:
# 1. Шрифт, лежащий рядом с кодом в самом репозитории (bundled) —
# так Space не зависит от сети во время работы и от блокировок GitHub.
bundled_font_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "font.ttf")
if os.path.exists(bundled_font_path):
return bundled_font_path
# 2. Кэш, куда мог быть скачан шрифт в предыдущем запуске
local_font_path = "font.ttf"
if os.path.exists(local_font_path):
return local_font_path
# 3. Стандартные системные шрифты (для локального ПК)
candidates = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"C:/Windows/Fonts/arial.ttf",
"/System/Library/Fonts/Helvetica.ttc",
]
for path in candidates:
if os.path.exists(path):
return path
# 4. Если ничего не нашли — пробуем скачать (как резервный вариант,
# не гарантирован, т.к. на некоторых Spaces исходящий трафик ограничен)
try:
print("[Font] Системные шрифты не найдены. Скачиваю Roboto для поддержки русского языка...")
font_url = "https://github.com/googlefonts/roboto/raw/main/src/hinted/Roboto-Regular.ttf"
urllib.request.urlretrieve(font_url, local_font_path)
return local_font_path
except Exception as e:
print(f"[Font] Не удалось скачать шрифт: {e}")
return None
def find_font_size(text: str, box_width: int, box_height: int, font_path: str = None) -> ImageFont.FreeTypeFont:
if font_path is None:
font_path = find_system_font()
size = box_height
while size > 6:
try:
if font_path:
font = ImageFont.truetype(font_path, size)
else:
return ImageFont.load_default()
except Exception:
return ImageFont.load_default()
bbox = font.getbbox(text)
text_width = bbox[2] - bbox[0]
if text_width <= box_width:
return font
size -= 1
try:
return ImageFont.truetype(font_path, 8) if font_path else ImageFont.load_default()
except Exception:
return ImageFont.load_default()
def replace_line_in_image(
image: Image.Image,
translated_text: str,
font_path: str = None,
margin: int = 10,
) -> Image.Image:
"""
Старое поведение: закрашивает ВСЮ картинку цветом фона и кладёт
одну строку перевода по центру. Оставлено для обратной совместимости
и для случаев, когда картинка изначально состоит из одной строки текста
на однородном фоне (как в тестовых/тренировочных данных).
Для картинок с несколькими строками или посторонним контентом
используйте replace_lines_in_image ниже — она не трогает то, что
не является текстом.
"""
result = image.convert("RGB").copy()
draw = ImageDraw.Draw(result)
bg_color = get_background_color(image)
draw.rectangle((0, 0, result.width, result.height), fill=bg_color)
text_color = get_text_color(bg_color)
box_width = result.width - 2 * margin
box_height = result.height - 2 * margin
font = find_font_size(translated_text, box_width, box_height, font_path)
bbox = font.getbbox(translated_text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (result.width - text_width) / 2
y = (result.height - text_height) / 2 - bbox[1]
draw.text((x, y), translated_text, fill=text_color, font=font)
return result
def replace_lines_in_image(
image: Image.Image,
line_boxes: list[tuple[int, int, int, int]],
translated_lines: list[str],
font_path: str = None,
padding: int = 4,
) -> Image.Image:
"""
Точечная замена: закрашивает и перерисовывает только те области,
где реально был найден текст (по одному прямоугольнику на строку),
остальная часть картинки остаётся нетронутой. Это чинит основной
недостаток replace_line_in_image — потерю всего, что не было текстом.
"""
result = image.convert("RGB").copy()
draw = ImageDraw.Draw(result)
for (left, top, right, bottom), new_text in zip(line_boxes, translated_lines):
l = max(0, left - padding)
t = max(0, top - padding)
r = min(result.width, right + padding)
b = min(result.height, bottom + padding)
bg_color = get_background_color(image.crop((l, t, r, b)))
draw.rectangle((l, t, r, b), fill=bg_color)
text_color = get_text_color(bg_color)
box_width = r - l
box_height = b - t
font = find_font_size(new_text, box_width, box_height, font_path)
bbox = font.getbbox(new_text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = l + (box_width - text_width) / 2
y = t + (box_height - text_height) / 2 - bbox[1]
draw.text((x, y), new_text, fill=text_color, font=font)
return result
def replace_words_in_image(
image: Image.Image,
word_boxes: list[tuple],
translated_words: list[str],
font_path: str = None,
) -> Image.Image:
result = image.convert("RGB").copy()
draw = ImageDraw.Draw(result)
for box, new_text in zip(word_boxes, translated_words):
left, top, right, bottom = box
bg_color = get_background_color(image.crop(box))
draw.rectangle(box, fill=bg_color)
text_color = get_text_color(bg_color)
box_width = right - left
box_height = bottom - top
font = find_font_size(new_text, box_width, box_height, font_path)
bbox = font.getbbox(new_text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = left + (box_width - text_width) / 2
y = top + (box_height - text_height) / 2 - bbox[1]
draw.text((x, y), new_text, fill=text_color, font=font)
return result