Files
ticket-system/backend/core/pdf_generator.py
2026-03-10 12:11:38 +00:00

184 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Генерирует горизонтальный PDF-билет (600×250 pt).
Поддержка кириллицы: ищет системный TTF-шрифт; при неудаче — транслитерация.
"""
import io
import os
import qrcode
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
# ─── Cyrillic font detection ──────────────────────────────────────────────────
_FONT_CANDIDATES = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
]
_FONT_NAME = "Helvetica" # fallback
for _path in _FONT_CANDIDATES:
if os.path.exists(_path):
try:
pdfmetrics.registerFont(TTFont("CyrFont", _path))
_FONT_NAME = "CyrFont"
except Exception:
pass
break
# ─── Transliteration fallback (used when no Cyrillic font is found) ──────────
_TRANSLIT_MAP: dict[str, str] = {
"а": "a", "б": "b", "в": "v", "г": "g", "д": "d",
"е": "e", "ё": "yo", "ж": "zh", "з": "z", "и": "i",
"й": "y", "к": "k", "л": "l", "м": "m", "н": "n",
"о": "o", "п": "p", "р": "r", "с": "s", "т": "t",
"у": "u", "ф": "f", "х": "kh", "ц": "ts", "ч": "ch",
"ш": "sh", "щ": "shch","ъ": "", "ы": "y", "ь": "",
"э": "e", "ю": "yu", "я": "ya",
}
def _safe(text: str) -> str:
"""Pass text through unchanged if a Cyrillic font is registered; otherwise transliterate."""
if _FONT_NAME != "Helvetica":
return text
result: list[str] = []
for ch in text:
lower = ch.lower()
if lower in _TRANSLIT_MAP:
t = _TRANSLIT_MAP[lower]
result.append(t.capitalize() if ch.isupper() and t else t)
else:
result.append(ch)
return "".join(result)
# ─── Color helpers ────────────────────────────────────────────────────────────
def _white(c: canvas.Canvas) -> None:
c.setFillColorRGB(1.0, 1.0, 1.0)
def _muted(c: canvas.Canvas) -> None:
c.setFillColorRGB(0.67, 0.67, 0.67) # #aaaaaa
def _accent(c: canvas.Canvas) -> None:
c.setFillColorRGB(0.89, 0.15, 0.21) # #e32636
# ─── Main generator ───────────────────────────────────────────────────────────
def generate_qr_ticket(
ticket_id: int,
title: str,
date_str: str,
sector: str,
row: int,
number: int,
price: int,
secret_token: str,
) -> bytes:
"""
Renders a landscape ticket (600×250 pt) and returns PDF bytes.
Left panel — event info + seat details (x 30‥410)
Right panel — QR code (x 420, y 50, 150×150)
"""
W, H = 600, 250
buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=(W, H))
# ── Background ──
c.setFillColorRGB(0.1, 0.1, 0.1)
c.rect(0, 0, W, H, fill=1, stroke=0)
# ── Accent top bar ──
c.setFillColorRGB(0.89, 0.15, 0.21)
c.rect(0, H - 6, W, 6, fill=1, stroke=0)
# ── Ticket ID badge (top-right of left panel) ──
_muted(c)
c.setFont(_FONT_NAME, 9)
c.drawRightString(400, H - 20, f"#{ticket_id}")
# ── Tournament title ──
_white(c)
c.setFont(_FONT_NAME, 20)
c.drawString(30, H - 50, _safe(title))
# ── Date & venue ──
_white(c)
c.setFont(_FONT_NAME, 12)
c.drawString(30, H - 72, _safe(date_str))
_muted(c)
c.setFont(_FONT_NAME, 12)
c.drawString(30, H - 90, _safe("Место: Main Arena, Москва"))
# ── Separator ──
c.setStrokeColorRGB(0.25, 0.25, 0.25)
c.setLineWidth(0.8)
c.line(30, H - 105, 400, H - 105)
# ── Seat details grid ──
labels = [
(_safe("Сектор"), str(sector)),
(_safe("Ряд"), str(row)),
(_safe("Место"), str(number)),
(_safe("Цена"), f"{price:,} RUB".replace(",", " ")),
]
col_w = 90
x_start = 30
y_label = H - 130
y_value = H - 148
for i, (label, value) in enumerate(labels):
x = x_start + i * col_w
_muted(c)
c.setFont(_FONT_NAME, 9)
c.drawString(x, y_label, label)
_white(c)
c.setFont(_FONT_NAME, 14)
c.drawString(x, y_value, value)
# ── Perforated divider line (left panel / QR panel) ──
c.setStrokeColorRGB(0.25, 0.25, 0.25)
c.setDash(4, 4)
c.setLineWidth(0.8)
c.line(415, 15, 415, H - 15)
c.setDash() # reset dash
# ── Scan hint ──
_muted(c)
c.setFont(_FONT_NAME, 8)
c.drawCentredString(495, 30, _safe("Сканировать при входе"))
# ── QR code ──
qr_data = f"https://openticket.artifitial.ru/scanner?token={secret_token}"
qr = qrcode.QRCode(box_size=5, border=1, error_correction=qrcode.constants.ERROR_CORRECT_M)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill_color="white", back_color="#1a1a1a")
img_buf = io.BytesIO()
img.save(img_buf, format="PNG")
img_buf.seek(0)
c.drawImage(ImageReader(img_buf), 420, 50, width=150, height=150)
c.showPage()
c.save()
buffer.seek(0)
return buffer.read()