phase 3 22 qr-code-renew
This commit is contained in:
@@ -1,51 +1,185 @@
|
||||
"""
|
||||
Генерирует горизонтальный PDF-билет (600×250 pt).
|
||||
Поддержка кириллицы: ищет системный TTF-шрифт; при неудаче — транслитерация.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
|
||||
import qrcode
|
||||
from reportlab.lib.pagesizes import landscape, A5
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
def generate_qr_ticket(ticket_id: int, user_id: int, seat_id: int = 0) -> bytes:
|
||||
# ─── 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,
|
||||
) -> 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()
|
||||
width, height = landscape(A5)
|
||||
c = canvas.Canvas(buffer, pagesize=(width, height))
|
||||
c = canvas.Canvas(buffer, pagesize=(W, H))
|
||||
|
||||
# Темный фон (под дизайн фронтенда)
|
||||
c.setFillColor(colors.HexColor("#121212"))
|
||||
c.rect(0, 0, width, height, fill=1, stroke=0)
|
||||
# ── Background ──
|
||||
c.setFillColorRGB(0.1, 0.1, 0.1)
|
||||
c.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
|
||||
# Белая карточка билета
|
||||
margin = 20
|
||||
c.setFillColor(colors.white)
|
||||
c.roundRect(margin, margin, width - 2*margin, height - 2*margin, 10, 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)
|
||||
|
||||
# Текст
|
||||
c.setFillColor(colors.black)
|
||||
c.setFont("Helvetica-Bold", 28)
|
||||
c.drawString(margin + 30, height - margin - 50, "EVENT TICKET")
|
||||
# ── Ticket ID badge (top-right of left panel) ──
|
||||
_muted(c)
|
||||
c.setFont(_FONT_NAME, 9)
|
||||
c.drawRightString(400, H - 20, f"#{ticket_id}")
|
||||
|
||||
c.setFont("Helvetica", 14)
|
||||
c.drawString(margin + 30, height - margin - 100, f"Ticket ID: {ticket_id}")
|
||||
c.drawString(margin + 30, height - margin - 130, f"User ID: {user_id}")
|
||||
if seat_id:
|
||||
c.drawString(margin + 30, height - margin - 160, f"Seat ID: {seat_id}")
|
||||
# ── Tournament title ──
|
||||
_white(c)
|
||||
c.setFont(_FONT_NAME, 20)
|
||||
c.drawString(30, H - 50, _safe(title))
|
||||
|
||||
c.setFont("Helvetica-Oblique", 10)
|
||||
c.setFillColor(colors.gray)
|
||||
c.drawString(margin + 30, margin + 30, "Scan this QR code at the entrance.")
|
||||
# ── Date & venue ──
|
||||
_white(c)
|
||||
c.setFont(_FONT_NAME, 12)
|
||||
c.drawString(30, H - 72, _safe(date_str))
|
||||
|
||||
# QR-код
|
||||
qr = qrcode.QRCode(box_size=5, border=1)
|
||||
qr.add_data(f"TICKET:{ticket_id}|USER:{user_id}|SEAT:{seat_id}")
|
||||
_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 = json.dumps(
|
||||
{"id": ticket_id, "t": title, "s": sector, "r": row, "m": number},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
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="black", back_color="white")
|
||||
|
||||
# Буферизация картинки для ReportLab
|
||||
img_buffer = io.BytesIO()
|
||||
img.save(img_buffer, format="PNG")
|
||||
img_buffer.seek(0)
|
||||
|
||||
c.drawImage(ImageReader(img_buffer), width - margin - 160, margin + 30, width=130, height=130)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user