Spaces:
Sleeping
Sleeping
Download image_editor.py from teuchi/dumm-image-translator: direct link, hf CLI and curl.
- Browser
- Download file 7.33 kB
-
https://huggingface.co/spaces/teuchi/dumm-image-translator/resolve/main/image_editor.py
- Command line
-
hf download hf://spaces/teuchi/dumm-image-translator/image_editor.py
-
curl -L -o image_editor.py https://huggingface.co/spaces/teuchi/dumm-image-translator/resolve/main/image_editor.py
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 | |