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