"""Генератор синтетических якутских строк/страниц. Два домена: 'book' (чистая современная книга) и 'news1940' (убитая высокая печать). """ import json, os, random, math import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps SAH_EXTRA = "ҔҕҤҥӨөҺһҮү" # --- отбор шрифтов: только те, что реально покрывают алфавит --- def load_fonts(kind): rows = json.load(open("data/fonts/ok_fonts.json")) rows = [r for r in rows if r["full"]] if kind == "book": want = ["PT Serif", "Times New Roman", "Charter", "Iowan Old Style", "Superclarendon", "PT Sans", "Open Sans", "Arial", "Tahoma"] else: # газета — узкие/плотные гротески и серифы want = ["PT Serif", "Times New Roman", "Charter", "Superclarendon", "PT Sans", "Open Sans Cond", "DIN Condensed", "Arial"] out = [r for r in rows if any(w.lower() in r["name"].lower() for w in want)] assert out, "нет подходящих шрифтов" return out # --- проверка: шрифт реально рисует спец-буквы, а не .notdef --- def renders_extra(fontpath, idx, size=40): f = ImageFont.truetype(fontpath, size, index=idx) ref = None for ch in SAH_EXTRA: im = Image.new("L", (size*2, size*2), 255) ImageDraw.Draw(im).text((5, 5), ch, font=f, fill=0) a = np.array(im) if a.min() > 200: # ничего не нарисовалось return False # все спец-буквы одинаковые => фолбэк на один и тот же tofu sig = a.tobytes() if ref is not None and sig == ref: return False ref = sig return True def degrade(img, domain, rng, _try=0): """Симуляция скана. img — L-mode. Гарантируем, что текст не выжжен.""" src = img a = np.asarray(img).astype(np.float32) / 255.0 if domain == "news1940": # 1) неровная прокраска: низкочастотный шум по яркости h, w = a.shape lf = rng.normal(0, 1, (max(2,h//24), max(2,w//24))).astype(np.float32) lf = np.array(Image.fromarray(lf).resize((w, h), Image.BICUBIC)) a = a + lf * rng.uniform(0.05, 0.18) # 2) растекание/разрыв штриха img2 = Image.fromarray(np.clip(a*255,0,255).astype(np.uint8)) if rng.random() < 0.6: img2 = img2.filter(ImageFilter.MinFilter(3)) # ink spread (темнее) if rng.random() < 0.45: img2 = img2.filter(ImageFilter.MaxFilter(3)) # разрыв штриха a = np.asarray(img2).astype(np.float32)/255. # 3) серо-жёлтая бумага + просвет с обратной стороны a = a * rng.uniform(0.80, 0.95) + rng.uniform(0.05, 0.18) blur = rng.uniform(0.15, 0.7) # высокая печать даёт жёсткий край, не мыло else: a = a * rng.uniform(0.92, 1.0) + rng.uniform(0.0, 0.06) blur = rng.uniform(0.2, 0.9) img = Image.fromarray(np.clip(a*255, 0, 255).astype(np.uint8)) img = img.filter(ImageFilter.GaussianBlur(blur)) a = np.asarray(img).astype(np.float32) a += rng.normal(0, rng.uniform(3, 14) if domain=="news1940" else rng.uniform(1,6), a.shape) img = Image.fromarray(np.clip(a,0,255).astype(np.uint8)) if rng.random() < 0.7: # перекос img = img.rotate(rng.uniform(-0.8, 0.8), resample=Image.BICUBIC, fillcolor=235, expand=False) # контроль: контраст текст/фон не должен схлопнуться a = np.asarray(img).astype(np.float32) ink = float(np.percentile(a, 2)); paper = float(np.percentile(a, 90)) if paper - ink < 55 and _try < 4: return degrade(src, "book" if _try >= 2 else domain, rng, _try + 1) return img def render_line(text, font, pad=10, rng=None, domain="book"): f = ImageFont.truetype(font["path"], font["size"], index=font["idx"]) tmp = Image.new("L", (10,10), 255) box = ImageDraw.Draw(tmp).textbbox((0,0), text, font=f) W, H = box[2]-box[0]+2*pad, box[3]-box[1]+2*pad im = Image.new("L", (W, H), 255) ImageDraw.Draw(im).text((pad-box[0], pad-box[1]), text, font=f, fill=rng.integers(0,45)) return degrade(im, domain, rng)