""" Генерирует горизонтальный 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()