File size: 7,329 Bytes
e53e505
 
 
31a7f96
e53e505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe5a7d8
 
 
 
 
 
 
31a7f96
 
 
fe5a7d8
 
e53e505
 
 
 
 
 
 
 
fe5a7d8
 
 
31a7f96
 
 
 
 
 
 
 
e53e505
 
 
 
 
 
 
 
 
 
 
 
31a7f96
e53e505
31a7f96
e53e505
 
 
 
 
 
 
 
31a7f96
 
 
 
e53e505
 
 
 
 
 
 
 
fe5a7d8
 
 
 
 
 
 
 
 
e53e505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe5a7d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e53e505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe5a7d8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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