"""Встроенные шаблоны и генерация фонов.""" from __future__ import annotations from pathlib import Path from PIL import Image, ImageDraw from .config import TEMPLATES_DIR GOLD = (198, 158, 45) NAVY = (27, 42, 74) MAROON = (122, 31, 43) CREAM = (255, 253, 247) CLASSIC_PAGE = {"width": 1754, "height": 1240} # A4 landscape @ ~150dpi LETTER_PAGE = {"width": 1240, "height": 1754} # A4 portrait @ ~150dpi # Меняйте при правке встроенных шаблонов — тогда они обновятся у пользователей. BUILTIN_VERSION = "2" def _diamond(d: ImageDraw.ImageDraw, cx: float, cy: float, r: float, fill) -> None: d.polygon([(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)], fill=fill) def _frame(img: Image.Image, outer: int = 34) -> ImageDraw.ImageDraw: W, H = img.size d = ImageDraw.Draw(img) d.rectangle([outer, outer, W - outer, H - outer], outline=GOLD, width=8) d.rectangle([outer + 24, outer + 24, W - outer - 24, H - outer - 24], outline=NAVY, width=2) d.rectangle([outer + 34, outer + 34, W - outer - 34, H - outer - 34], outline=GOLD, width=1) inset = outer + 24 for cx, cy in [(inset, inset), (W - inset, inset), (inset, H - inset), (W - inset, H - inset)]: _diamond(d, cx, cy, 18, GOLD) _diamond(d, cx, cy, 8, NAVY) _diamond(d, cx, cy, 3, CREAM) return d def draw_classic_bg(path: Path) -> None: W, H = CLASSIC_PAGE["width"], CLASSIC_PAGE["height"] img = Image.new("RGB", (W, H), CREAM) d = _frame(img) # разделитель под заголовком d.line([(W * 0.30, H * 0.245), (W * 0.70, H * 0.245)], fill=GOLD, width=3) _diamond(d, W * 0.5, H * 0.245, 9, GOLD) # линия для подписи d.line([(W * 0.42, H * 0.925), (W * 0.58, H * 0.925)], fill=NAVY, width=2) img.save(path, "PNG") def draw_letter_bg(path: Path) -> None: W, H = LETTER_PAGE["width"], LETTER_PAGE["height"] img = Image.new("RGB", (W, H), CREAM) d = _frame(img) d.line([(W * 0.26, H * 0.205), (W * 0.74, H * 0.205)], fill=GOLD, width=3) _diamond(d, W * 0.5, H * 0.205, 9, GOLD) d.line([(W * 0.42, H * 0.925), (W * 0.58, H * 0.925)], fill=NAVY, width=2) img.save(path, "PNG") def ensure_assets() -> None: """Создать фоны встроенных шаблонов, если их ещё нет.""" builtin_dir = TEMPLATES_DIR / "builtin" builtin_dir.mkdir(parents=True, exist_ok=True) classic = builtin_dir / "classic.png" letter = builtin_dir / "letter.png" if not classic.exists(): draw_classic_bg(classic) if not letter.exists(): draw_letter_bg(letter) def builtin_templates() -> list[dict]: return [ { "slug": "classic", "title": "СЕРТИФИКАТ", "kind": "image", "background": "builtin/classic.png", "page": CLASSIC_PAGE, "defaults": { "title": "СЕРТИФИКАТ", "body": "за успешное прохождение программы", }, "layout": [ {"key": "title", "x": 0.5, "y": 0.175, "size": 96, "font": "serif-bold", "color": "#1B2A4A", "align": "center", "uppercase": True, "letter_spacing": 10, "max_width": 0.9}, {"text": "настоящий сертификат подтверждает, что", "x": 0.5, "y": 0.315, "size": 34, "font": "serif-italic", "color": "#5a5a5a", "align": "center"}, {"key": "fio", "x": 0.5, "y": 0.435, "size": 96, "font": "serif-bold", "color": "#7A1F2B", "align": "center", "max_width": 0.82}, {"key": "body", "x": 0.5, "y": 0.645, "size": 40, "font": "serif", "color": "#333333", "align": "center", "max_width": 0.72, "line_spacing": 1.45}, {"text": "Дата выдачи", "x": 0.26, "y": 0.795, "size": 22, "font": "sans", "color": "#8a8a8a", "align": "center"}, {"key": "date", "x": 0.26, "y": 0.845, "size": 32, "font": "serif-bold", "color": "#1B2A4A", "align": "center"}, {"text": "Номер документа", "x": 0.74, "y": 0.795, "size": 22, "font": "sans", "color": "#8a8a8a", "align": "center"}, {"key": "number", "x": 0.74, "y": 0.845, "size": 32, "font": "serif-bold", "color": "#1B2A4A", "align": "center"}, ], }, { "slug": "letter", "title": "БЛАГОДАРСТВЕННОЕ ПИСЬМО", "kind": "image", "background": "builtin/letter.png", "page": LETTER_PAGE, "defaults": { "title": "БЛАГОДАРСТВЕННОЕ ПИСЬМО", "body": "за вклад в развитие и активное участие", }, "layout": [ {"key": "title", "x": 0.5, "y": 0.15, "size": 62, "font": "serif-bold", "color": "#1B2A4A", "align": "center", "uppercase": True, "letter_spacing": 6, "max_width": 0.86}, {"text": "объявляется благодарность", "x": 0.5, "y": 0.255, "size": 32, "font": "serif-italic", "color": "#5a5a5a", "align": "center"}, {"key": "fio", "x": 0.5, "y": 0.36, "size": 70, "font": "serif-bold", "color": "#7A1F2B", "align": "center", "max_width": 0.84}, {"key": "body", "x": 0.5, "y": 0.50, "size": 34, "font": "serif", "color": "#333333", "align": "center", "max_width": 0.74, "line_spacing": 1.5}, {"text": "Дата", "x": 0.27, "y": 0.80, "size": 22, "font": "sans", "color": "#8a8a8a", "align": "center"}, {"key": "date", "x": 0.27, "y": 0.85, "size": 30, "font": "serif-bold", "color": "#1B2A4A", "align": "center"}, {"text": "Номер", "x": 0.73, "y": 0.80, "size": 22, "font": "sans", "color": "#8a8a8a", "align": "center"}, {"key": "number", "x": 0.73, "y": 0.85, "size": 30, "font": "serif-bold", "color": "#1B2A4A", "align": "center"}, ], }, ]