supersam/docs/autocall/how-it-works.py

284 lines
9.5 KiB
Python
Raw 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.

#!/usr/bin/env python3
"""PDF-версия презентации «Автодозвон: как это работает» (тёмная тема, 16:9).
Содержание — из deck_content.py (то же, что в HTML-версии), здесь только вёрстка.
Текст, который не влезает в карточку, считается ошибкой: скрипт падает, а не рисует
поверх границ.
Запуск: python3 docs/autocall/how-it-works.py
"""
from pathlib import Path
from reportlab.lib.colors import Color, HexColor
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
import deck_content as content
FONT_DIR = Path("/usr/share/fonts/truetype/dejavu")
OUT = Path(__file__).with_name("autocall-how-it-works.pdf")
W, H = 960, 540
M = 46
BG = HexColor("#0B1220")
BG2 = HexColor("#101A2C")
CARD = HexColor("#131E33")
BORDER = HexColor("#25344F")
TEXT = HexColor("#E8EEF9")
DIM = HexColor("#9FB0CC")
TONES = {
"accent": HexColor("#4ADE80"),
"accent2": HexColor("#60A5FA"),
"warn": HexColor("#FBBF24"),
"danger": HexColor("#F87171"),
}
ACCENT = TONES["accent"]
pdfmetrics.registerFont(TTFont("DV", str(FONT_DIR / "DejaVuSans.ttf")))
pdfmetrics.registerFont(TTFont("DV-B", str(FONT_DIR / "DejaVuSans-Bold.ttf")))
c = canvas.Canvas(str(OUT), pagesize=(W, H))
c.setTitle("Автодозвон SuperSam — как это работает")
c.setAuthor("SuperSam")
slide_no = 0
CONTENT_W = W - 2 * M
def tone(name):
return TONES.get(name, ACCENT)
def wrap(text, font, size, max_w):
words, lines, cur = text.split(), [], ""
for word in words:
probe = f"{cur} {word}".strip()
if pdfmetrics.stringWidth(probe, font, size) <= max_w:
cur = probe
else:
if cur:
lines.append(cur)
cur = word
if cur:
lines.append(cur)
return lines
def bg():
c.setFillColor(BG)
c.rect(0, 0, W, H, stroke=0, fill=1)
c.setFillColor(BG2)
c.rect(0, H - 120, W, 120, stroke=0, fill=1)
c.setFillColor(Color(0.29, 0.85, 0.5, alpha=0.06))
c.circle(W - 90, H - 40, 150, stroke=0, fill=1)
def footer():
global slide_no
slide_no += 1
c.setFont("DV", 9)
c.setFillColor(DIM)
c.drawString(M, 22, "Автодозвон SuperSam · как это работает")
c.drawRightString(W - M, 22, f"{slide_no}")
c.setStrokeColor(BORDER)
c.setLineWidth(0.5)
c.line(M, 36, W - M, 36)
def header(title, sub=None, kicker=None):
y = H - 60
if kicker:
c.setFont("DV-B", 10)
c.setFillColor(ACCENT)
c.drawString(M, y + 22, kicker.upper())
c.setFont("DV-B", 26)
c.setFillColor(TEXT)
c.drawString(M, y - 12, title)
if sub:
c.setFont("DV", 12)
c.setFillColor(DIM)
c.drawString(M, y - 34, sub)
def card(x, y, w, h, accent=None, fill=CARD):
c.setFillColor(fill)
c.setStrokeColor(accent or BORDER)
c.setLineWidth(1)
c.roundRect(x, y, w, h, 12, stroke=1, fill=1)
def block_size(items, w, size=12, lead=18, title_size=14):
"""Сколько места займёт карточка: список строк и высота."""
wrapped = [wrap(item, "DV", size, w - 44) for item in items]
lines = sum(len(w_) for w_ in wrapped)
height = 50 + lines * lead + len(items) * 4
return wrapped, height
def draw_block(x, y, w, h, title, items, accent=ACCENT, size=12, lead=18, title_size=14):
wrapped, _ = block_size(items, w, size, lead, title_size)
card(x, y - h, w, h, accent=accent)
c.setFont("DV-B", title_size)
c.setFillColor(accent)
c.drawString(x + 16, y - 26, title)
cursor = y - 48
for wrapped_item in wrapped:
for i, line in enumerate(wrapped_item):
if i == 0:
c.setFillColor(accent)
c.circle(x + 20, cursor + 4, 2.4, stroke=0, fill=1)
c.setFont("DV", size)
c.setFillColor(TEXT)
c.drawString(x + 32, cursor, line)
cursor -= lead
cursor -= 4
if cursor < (y - h) - 2:
raise AssertionError(f"текст вышел за карточку «{title}»")
return y - h
def table_measure(rows, widths, size=11, lead=19):
"""Высота таблицы — чтобы можно было отцентровать блок по слайду."""
h = 0
for row in rows:
lines = max(len(wrap(str(cell), "DV", size, cw - 16)) for cell, cw in zip(row, widths))
h += lead + 8 + (lines - 1) * (lead - 4)
return h
def cards_row(cards, y, total_w=CONTENT_W, gap=14):
"""Ряд карточек одинаковой высоты; возвращает нижнюю границу ряда."""
n = len(cards)
w = (total_w - gap * (n - 1)) / n
heights = [block_size(card_["items"], w)[1] for card_ in cards]
h = max(heights)
x = M
for card_ in cards:
draw_block(x, y, w, h, card_["title"], card_["items"], accent=tone(card_.get("accent")))
x += w + gap
return y - h
def table(rows, widths, y, size=11, lead=19, head=True):
"""Таблица; возвращает нижнюю границу."""
for r_i, row in enumerate(rows):
lines_per_cell = [wrap(str(cell), "DV", size, cw - 16) for cell, cw in zip(row, widths)]
h = lead + 8 + (max(len(l) for l in lines_per_cell) - 1) * (lead - 4)
if head and r_i == 0:
card(M, y - h, CONTENT_W, h, accent=TONES["accent2"], fill=HexColor("#182640"))
cx = M
for (cell, cw), cell_lines in zip(zip(row, widths), lines_per_cell):
if head and r_i == 0:
cx += cw
continue
for i, line in enumerate(cell_lines):
c.setFont("DV", size)
c.setFillColor(DIM if (i == 0 and cx == M) else TEXT)
c.drawString(cx + 8, y - 21 - i * (lead - 4), line)
cx += cw
if head and r_i == 0:
cx = M
for cell, cw in zip(row, widths):
c.setFont("DV-B", size)
c.setFillColor(TEXT)
c.drawString(cx + 8, y - 21, str(cell))
cx += cw
y -= h
return y
def steps_row(steps, y):
n = len(steps)
gap = 14
w = (CONTENT_W - gap * (n - 1)) / n
h = 170
x = M
for i, (title, desc) in enumerate(steps):
accent = ACCENT if i < n - 1 else TONES["accent2"]
card(x, y - h, w, h, accent=accent)
c.setFont("DV-B", 12)
c.setFillColor(accent)
c.drawString(x + 14, y - 34, title)
c.setFont("DV", 11)
c.setFillColor(TEXT)
line_y = y - 58
for raw_line in desc.split("\n"):
for line in wrap(raw_line, "DV", 11, w - 28):
c.drawString(x + 14, line_y, line)
line_y -= 17
if i < n - 1:
c.setFillColor(DIM)
c.setFont("DV-B", 16)
c.drawString(x + w + 1, y - 72, "›")
x += w + gap
return y - h
def render(slide):
bg()
if slide["kind"] == "title":
c.setFont("DV-B", 11)
c.setFillColor(ACCENT)
c.drawString(M, H - 150, slide["kicker"])
c.setFont("DV-B", 40)
c.setFillColor(TEXT)
c.drawString(M, H - 210, slide["title"])
c.setFont("DV", 15)
c.setFillColor(DIM)
c.drawString(M, H - 244, slide["sub"])
card(M, 120, CONTENT_W, 150, accent=ACCENT)
for i, line in enumerate(slide["lines"]):
c.setFont("DV", 13)
c.setFillColor(TEXT if line else DIM)
c.drawString(M + 18, 240 - i * 20, line)
footer()
c.showPage()
return
header(slide["title"], slide.get("sub"))
kind = slide["kind"]
full = slide.get("full")
# Измеряем содержимое и центруем блок по вертикали: иначе внизу слайда
# остаётся пустая полоса, а на другом слайде текст поджимает футер.
if kind == "steps":
main_h = 170
elif kind in ("cards2full", "cards3full"):
n = len(slide["cards"])
w = (CONTENT_W - 14 * (n - 1)) / n
main_h = max(block_size(card_["items"], w)[1] for card_ in slide["cards"])
elif kind == "tablecard":
main_h = table_measure(slide["table"]["rows"], slide["table"]["widths"])
else:
raise AssertionError(f"неизвестный тип слайда: {kind}")
full_h = block_size(full["items"], CONTENT_W)[1] if full else 0
gap = 22 if full else 0
top, bottom_limit = H - 96, 56
total = main_h + (gap + full_h if full else 0)
y0 = top - max(0.0, (top - bottom_limit - total) / 2)
if kind == "steps":
bottom = steps_row(slide["steps"], y0)
elif kind in ("cards2full", "cards3full"):
bottom = cards_row(slide["cards"], y0)
else:
bottom = table(slide["table"]["rows"], slide["table"]["widths"], y0)
if full:
y = bottom - gap
if y - full_h < 44:
raise AssertionError(f"нижняя карточка не влезает на слайде «{slide['title']}»")
draw_block(M, y, CONTENT_W, full_h, full["title"], full["items"], accent=tone(full.get("accent")))
footer()
c.showPage()
for slide_ in content.ALL_SLIDES:
render(slide_)
c.save()
print(f"готово: {OUT} — слайдов {slide_no}")
print(f"вёрстка проверена: все карточки по размеру текста (проверено {slide_no} слайдов)")