Python
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.colors import HexColor
import textwrap
# Register fonts
pdfmetrics.registerFont(TTFont('Poppins-Bold', '/usr/share/fonts/truetype/google-fonts/Poppins-Bold.ttf'))
pdfmetrics.registerFont(TTFont('Poppins-Light', '/usr/share/fonts/truetype/google-fonts/Poppins-Light.ttf'))
pdfmetrics.registerFont(TTFont('Poppins-Regular', '/usr/share/fonts/truetype/google-fonts/Poppins-Regular.ttf'))
# Brand colors from brief
GRAPHITE = HexColor('#211d1c') # dark background
GOLD = HexColor('#FFDAA9') # primary accent
CREAM = HexColor('#FDECD4') # secondary warm
WHITE = HexColor('#FFFFFF')
WARM_MID = HexColor('#C4956A') # mid-tone for accents/lines
# Slide dimensions: 1080x1920 (9:16)
# BUT readable content area is 1080x1350 (4:5) centered vertically
# We'll create 1080x1920 px → convert pt: 1pt = 1px at 72dpi, but we work at 72dpi
# So: 1080 pt wide x 1920 pt tall
W = 1080
H = 1920
SAFE_TOP = (H - 1350) // 2 # 285 pt from top — start of readable area
SAFE_BOT = SAFE_TOP + 1350 # 1635 pt
PAD_X = 80
def draw_background(c, slide_idx):
"""Draw dark graphite background with subtle texture."""
c.setFillColor(GRAPHITE)
c.rect(0, 0, W, H, fill=1, stroke=0)
# Subtle grain dots for texture
import random
random.seed(slide_idx * 7)
c.setFillColor(HexColor('#2a2623'))
for _ in range(300):
x = random.randint(0, W)
y = random.randint(0, H)
r = random.uniform(0.5, 2)
c.circle(x, y, r, fill=1, stroke=0)
def draw_brand_frame(c):
"""Draw the safe-zone frame lines — gold horizontal bars."""
# Top gold line
c.setStrokeColor(GOLD)
c.setLineWidth(1.5)
c.line(PAD_X, H - SAFE_TOP, W - PAD_X, H - SAFE_TOP)
# Bottom gold line
c.line(PAD_X, H - SAFE_BOT, W - PAD_X, H - SAFE_BOT)
def draw_slide_number(c, num, total=8):
txt = f"{num:02d} / {total:02d}"
c.setFont('Poppins-Light', 22)
c.setFillColor(WARM_MID)
c.drawString(PAD_X, H - SAFE_TOP + 20, txt)
def draw_logo(c):
"""Small logo text bottom right inside safe zone."""
c.setFont('Poppins-Light', 20)
c.setFillColor(WARM_MID)
c.drawRightString(W - PAD_X, H - SAFE_BOT + 20, '@ekaterina_rubanova_design')
def wrap_text(c, text, font, size, max_width):
"""Returns list of lines."""
c.setFont(font, size)
words = text.split()
lines = []
current = ''
for w in words:
test = (current + ' ' + w).strip()
if c.stringWidth(test, font, size) <= max_width:
current = test
else:
if current:
lines.append(current)
current = w
if current:
lines.append(current)
return lines
def draw_multiline(c, text, font, size, color, x, y, max_width, leading=None):
"""Draw wrapped text, return y after last line."""
if leading is None:
leading = size * 1.35
lines = wrap_text(c, text, font, size, max_width)
c.setFont(font, size)
c.setFillColor(color)
for line in lines:
c.drawString(x, y, line)
y -= leading
return y
def draw_bullet_list(c, items, x, y, font, size, color, dot_color, max_width, leading=None):
"""Draw bullet list, return y after last item."""
if leading is None:
leading = size * 1.5
c.setFont(font, size)
for item in items:
# bullet dot
c.setFillColor(dot_color)
c.circle(x + 10, y + size * 0.3, 4, fill=1, stroke=0)
c.setFillColor(color)
# text, indented
y = draw_multiline(c, item, font, size, color, x + 28, y, max_width - 28, leading)
y -= 6
return y
def draw_error_number(c, num_str, x, y):
"""Large decorative error number background."""
c.setFont('Poppins-Bold', 180)
c.setFillColor(HexColor('#2c2826'))
c.drawString(x, y, num_str)
def gold_tag(c, text, x, y):
"""Draw a small gold tag/badge."""
tw = c.stringWidth(text, 'Poppins-Bold', 22)
pad = 14
rh = 36
c.setFillColor(GOLD)
c.roundRect(x, y, tw + pad*2, rh, 6, fill=1, stroke=0)
c.setFont('Poppins-Bold', 22)
c.setFillColor(GRAPHITE)
c.drawString(x + pad, y + 9, text)
return tw + pad*2
# ─── SLIDE DEFINITIONS ────────────────────────────────────────────────────────
def slide1(c):
"""HOOK slide."""
draw_background(c, 1)
draw_brand_frame(c)
draw_slide_number(c, 1)
draw_logo(c)
# Large decorative number
draw_error_number(c, '5', W - 200, H - SAFE_TOP - 460)
# Gold accent line
cy = H - SAFE_TOP - 80
c.setStrokeColor(GOLD)
c.setLineWidth(3)
c.line(PAD_X, cy, PAD_X + 80, cy)
# Pre-title tag
gold_tag(c, 'ДО РЕМОНТА', PAD_X, H - SAFE_TOP - 130)
cy = H - SAFE_TOP - 230
# Main headline
draw_multiline(c, '5 вещей, которые забывают', 'Poppins-Bold', 72, WHITE, PAD_X, cy, W - PAD_X*2 - 60, leading=82)
cy -= 82*2
draw_multiline(c, 'ПЕРЕД ремонтом', 'Poppins-Bold', 72, GOLD, PAD_X, cy, W - PAD_X*2, leading=82)
cy -= 110
# Subline
draw_multiline(c, 'А потом живут с этим годами.', 'Poppins-Light', 38, CREAM, PAD_X, cy, W - PAD_X*2, leading=52)
def slide2(c):
"""Ошибка №1 — Свет."""
draw_background(c, 2)
draw_brand_frame(c)
draw_slide_number(c, 2)
draw_logo(c)
draw_error_number(c, '1', W - 220, H - SAFE_TOP - 440)
cy = H - SAFE_TOP - 100
gold_tag(c, 'ОШИБКА №1', PAD_X, cy)
cy -= 90
draw_multiline(c, 'Свет продумали', 'Poppins-Bold', 68, WHITE, PAD_X, cy, W - PAD_X*2, leading=80)
cy -= 80
draw_multiline(c, 'после ремонта', 'Poppins-Bold', 68, GOLD, PAD_X, cy, W - PAD_X*2, leading=80)
cy -= 100
# Gold divider
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, W - PAD_X, cy)
cy -= 50
c.setFont('Poppins-Light', 32)
c.setFillColor(CREAM)
c.drawString(PAD_X, cy, 'В итоге:')
cy -= 52
bullets = [
'Тёмные углы в комнате',
'Выключатели в неудобных местах',
'Мало розеток',
'Одна люстра на всё помещение',
]
cy = draw_bullet_list(c, bullets, PAD_X, cy, 'Poppins-Light', 34, CREAM, GOLD, W - PAD_X*2, leading=52)
cy -= 40
# Tip box
c.setFillColor(HexColor('#2e2926'))
c.roundRect(PAD_X, cy - 80, W - PAD_X*2, 80, 10, fill=1, stroke=0)
c.setStrokeColor(GOLD)
c.setLineWidth(1.5)
c.roundRect(PAD_X, cy - 80, W - PAD_X*2, 80, 10, fill=0, stroke=1)
c.setFont('Poppins-Regular', 30)
c.setFillColor(GOLD)
c.drawString(PAD_X + 24, cy - 50, 'Освещение проектируют до начала электрики')
def slide3(c):
"""Ошибка №2 — Сантехника."""
draw_background(c, 3)
draw_brand_frame(c)
draw_slide_number(c, 3)
draw_logo(c)
draw_error_number(c, '2', W - 220, H - SAFE_TOP - 440)
cy = H - SAFE_TOP - 100
gold_tag(c, 'ОШИБКА №2', PAD_X, cy)
cy -= 90
draw_multiline(c, 'Сначала плитка —', 'Poppins-Bold', 66, WHITE, PAD_X, cy, W - PAD_X*2, leading=78)
cy -= 78
draw_multiline(c, 'потом сантехника', 'Poppins-Bold', 66, GOLD, PAD_X, cy, W - PAD_X*2, leading=78)
cy -= 98
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, W - PAD_X, cy)
cy -= 50
c.setFont('Poppins-Light', 32)
c.setFillColor(CREAM)
c.drawString(PAD_X, cy, 'В результате:')
cy -= 52
bullets = [
'Неудобный душ',
'Проблемы с инсталляцией унитаза',
'Ошибки с высотой смесителей',
'Плохая вентиляция',
]
cy = draw_bullet_list(c, bullets, PAD_X, cy, 'Poppins-Light', 34, CREAM, GOLD, W - PAD_X*2, leading=52)
cy -= 40
c.setFillColor(HexColor('#2e2926'))
c.roundRect(PAD_X, cy - 80, W - PAD_X*2, 80, 10, fill=1, stroke=0)
c.setStrokeColor(GOLD)
c.setLineWidth(1.5)
c.roundRect(PAD_X, cy - 80, W - PAD_X*2, 80, 10, fill=0, stroke=1)
c.setFont('Poppins-Regular', 30)
c.setFillColor(GOLD)
c.drawString(PAD_X + 24, cy - 50, 'Мокрые зоны продумываются заранее')
def slide4(c):
"""Ошибка №3 — Материалы."""
draw_background(c, 4)
draw_brand_frame(c)
draw_slide_number(c, 4)
draw_logo(c)
draw_error_number(c, '3', W - 220, H - SAFE_TOP - 440)
cy = H - SAFE_TOP - 100
gold_tag(c, 'ОШИБКА №3', PAD_X, cy)
cy -= 90
draw_multiline(c, 'Выбирали материалы', 'Poppins-Bold', 64, WHITE, PAD_X, cy, W - PAD_X*2, leading=76)
cy -= 76
draw_multiline(c, 'только по картинке', 'Poppins-Bold', 64, GOLD, PAD_X, cy, W - PAD_X*2, leading=76)
cy -= 96
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, W - PAD_X, cy)
cy -= 50
c.setFont('Poppins-Light', 32)
c.setFillColor(CREAM)
c.drawString(PAD_X, cy, 'Без проверки:')
cy -= 52
bullets = [
'Как ведёт себя при вашем свете',
'Практичность в уходе',
'Текстура в реальности',
'Ощущение в пространстве',
]
cy = draw_bullet_list(c, bullets, PAD_X, cy, 'Poppins-Light', 34, CREAM, GOLD, W - PAD_X*2, leading=52)
cy -= 40
c.setFillColor(HexColor('#2e2926'))
c.roundRect(PAD_X, cy - 100, W - PAD_X*2, 100, 10, fill=1, stroke=0)
c.setStrokeColor(GOLD)
c.setLineWidth(1.5)
c.roundRect(PAD_X, cy - 100, W - PAD_X*2, 100, 10, fill=0, stroke=1)
draw_multiline(c, 'Красивое в магазине может раздражать дома каждый день', 'Poppins-Regular', 30, GOLD, PAD_X + 24, cy - 32, W - PAD_X*2 - 50, leading=40)
def slide5(c):
"""Ошибка №4 — Хранение."""
draw_background(c, 5)
draw_brand_frame(c)
draw_slide_number(c, 5)
draw_logo(c)
draw_error_number(c, '4', W - 220, H - SAFE_TOP - 440)
cy = H - SAFE_TOP - 100
gold_tag(c, 'ОШИБКА №4', PAD_X, cy)
cy -= 90
draw_multiline(c, 'Хранение не', 'Poppins-Bold', 68, WHITE, PAD_X, cy, W - PAD_X*2, leading=80)
cy -= 80
draw_multiline(c, 'продумали заранее', 'Poppins-Bold', 68, GOLD, PAD_X, cy, W - PAD_X*2, leading=80)
cy -= 100
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, W - PAD_X, cy)
cy -= 50
c.setFont('Poppins-Light', 32)
c.setFillColor(CREAM)
c.drawString(PAD_X, cy, 'Потом появляются:')
cy -= 52
bullets = [
'Визуальный шум',
'Коробки и вещи на виду',
'Ощущение вечного беспорядка',
]
cy = draw_bullet_list(c, bullets, PAD_X, cy, 'Poppins-Light', 34, CREAM, GOLD, W - PAD_X*2, leading=52)
cy -= 55
c.setFillColor(HexColor('#2e2926'))
c.roundRect(PAD_X, cy - 100, W - PAD_X*2, 100, 10, fill=1, stroke=0)
c.setStrokeColor(GOLD)
c.setLineWidth(1.5)
c.roundRect(PAD_X, cy - 100, W - PAD_X*2, 100, 10, fill=0, stroke=1)
draw_multiline(c, 'Красивый интерьер — это не только стиль. Это сценарий жизни', 'Poppins-Regular', 30, GOLD, PAD_X + 24, cy - 32, W - PAD_X*2 - 50, leading=40)
def slide6(c):
"""Ошибка №5 — Подготовка."""
draw_background(c, 6)
draw_brand_frame(c)
draw_slide_number(c, 6)
draw_logo(c)
draw_error_number(c, '5', W - 220, H - SAFE_TOP - 440)
cy = H - SAFE_TOP - 100
gold_tag(c, 'ОШИБКА №5', PAD_X, cy)
cy -= 90
draw_multiline(c, 'Начали ремонт без', 'Poppins-Bold', 65, WHITE, PAD_X, cy, W - PAD_X*2, leading=78)
cy -= 78
draw_multiline(c, 'полной подготовки', 'Poppins-Bold', 65, GOLD, PAD_X, cy, W - PAD_X*2, leading=78)
cy -= 98
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, W - PAD_X, cy)
cy -= 50
c.setFont('Poppins-Light', 32)
c.setFillColor(CREAM)
c.drawString(PAD_X, cy, 'Без:')
cy -= 52
bullets = [
'Чертежей и плана',
'Сметы',
'Договора с подрядчиком',
'Финальной закупки материалов',
]
cy = draw_bullet_list(c, bullets, PAD_X, cy, 'Poppins-Light', 34, CREAM, GOLD, W - PAD_X*2, leading=52)
cy -= 40
c.setFillColor(HexColor('#2e2926'))
c.roundRect(PAD_X, cy - 80, W - PAD_X*2, 80, 10, fill=1, stroke=0)
c.setStrokeColor(GOLD)
c.setLineWidth(1.5)
c.roundRect(PAD_X, cy - 80, W - PAD_X*2, 80, 10, fill=0, stroke=1)
c.setFont('Poppins-Regular', 30)
c.setFillColor(GOLD)
c.drawString(PAD_X + 24, cy - 50, 'Здесь начинаются самые дорогие ошибки')
def slide7(c):
"""Emotional / manifesto slide."""
draw_background(c, 7)
draw_brand_frame(c)
draw_slide_number(c, 7)
draw_logo(c)
# Big decorative quote marks
c.setFont('Poppins-Bold', 200)
c.setFillColor(HexColor('#2c2826'))
c.drawString(PAD_X - 10, H - SAFE_TOP - 220, '\u201c')
cy = H - SAFE_TOP - 140
draw_multiline(c, 'Хороший интерьер —', 'Poppins-Bold', 66, WHITE, PAD_X, cy, W - PAD_X*2, leading=80)
cy -= 80
draw_multiline(c, 'это не про «красиво»', 'Poppins-Bold', 66, GOLD, PAD_X, cy, W - PAD_X*2, leading=80)
cy -= 110
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, PAD_X + 100, cy)
cy -= 50
items = [
'Удобство каждый день',
'Свет, который не раздражает',
'Пространство, где можно дышать',
'Ощущения, которые остаются',
]
c.setFont('Poppins-Light', 36)
for item in items:
c.setFillColor(GOLD)
c.drawString(PAD_X, cy, '—')
c.setFillColor(CREAM)
c.drawString(PAD_X + 35, cy, item)
cy -= 56
cy -= 20
draw_multiline(c, 'Это жизнь, которая будет\nпроисходить в этом пространстве.', 'Poppins-Regular', 34, HexColor('#FDECD4'), PAD_X, cy, W - PAD_X*2, leading=50)
def slide8(c):
"""CTA slide."""
draw_background(c, 8)
draw_brand_frame(c)
draw_slide_number(c, 8)
draw_logo(c)
# Gold large accent block
c.setFillColor(GOLD)
c.rect(0, H - SAFE_TOP - 420, W, 4, fill=1, stroke=0)
cy = H - SAFE_TOP - 100
c.setFont('Poppins-Light', 34)
c.setFillColor(CREAM)
c.drawString(PAD_X, cy, 'Я подготовила')
cy -= 55
# Big highlight
draw_multiline(c, 'ЧЕК-ЛИСТ', 'Poppins-Bold', 110, GOLD, PAD_X, cy, W - PAD_X*2, leading=120)
cy -= 130
draw_multiline(c, '42 пункта', 'Poppins-Bold', 72, WHITE, PAD_X, cy, W - PAD_X*2, leading=82)
cy -= 90
c.setFont('Poppins-Light', 34)
c.setFillColor(CREAM)
draw_multiline(c, 'которые помогают понять, готовы ли вы к ремонту', 'Poppins-Light', 34, CREAM, PAD_X, cy, W - PAD_X*2, leading=48)
cy -= 110
c.setStrokeColor(GOLD)
c.setLineWidth(1)
c.line(PAD_X, cy, W - PAD_X, cy)
cy -= 60
# CTA box
c.setFillColor(GOLD)
c.roundRect(PAD_X, cy - 100, W - PAD_X*2, 100, 14, fill=1, stroke=0)
c.setFont('Poppins-Bold', 46)
c.setFillColor(GRAPHITE)
txt = 'Напишите: «ЧЕК-ЛИСТ»'
tw = c.stringWidth(txt, 'Poppins-Bold', 46)
c.drawString((W - tw) / 2, cy - 60, txt)
cy -= 130
c.setFont('Poppins-Light', 30)
c.setFillColor(CREAM)
draw_multiline(c, 'и я отправлю материалы в директ', 'Poppins-Light', 30, CREAM, PAD_X, cy, W - PAD_X*2, leading=44)
# ─── BUILD PDF ────────────────────────────────────────────────────────────────
output_path = '/mnt/user-data/outputs/rubanova_carousel_remont.pdf'
c = canvas.Canvas(output_path, pagesize=(W, H))
c.setTitle('Рубанова — 5 ошибок до ремонта')
slides = [slide1, slide2, slide3, slide4, slide5, slide6, slide7, slide8]
for fn in slides:
fn(c)
c.showPage()
c.save()
print(f'Done → {output_path}')