Initial commit: certgen — сервис генерации сертификатов
This commit is contained in:
commit
49bac43e16
23 files changed
+3046
No files matched your search
@@ -0,0 +1,136 @@
|
||||
"""Встроенные шаблоны и генерация фонов."""
|
||||
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"},
|
||||
],
|
||||
},
|
||||
]
|
||||
Reference in new issue
Block a user