188 lines
5.7 KiB
Python
188 lines
5.7 KiB
Python
"""
|
||
Генерирует горизонтальный PDF-билет (600×250 pt).
|
||
Поддержка кириллицы: ищет системный TTF-шрифт; при неудаче — транслитерация.
|
||
"""
|
||
import io
|
||
import json
|
||
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,
|
||
) -> 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 = 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="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()
|