// Python Dev
Генерация картинок по шаблону: SVG + Python + Playwright
Опубликовано 17.06.2026
Одна из задач в проекте по рассылке авиабилетов — автоматически генерировать карточки с предложениями для публикации в пабликах. Цена, маршрут, дата — данные меняются, картинка должна обновляться по расписанию без участия человека.
Задача выглядит простой, но решают её по-разному. Часто — сложнее, чем нужно.
Чем можно решить эту задачу
Прежде чем объяснять выбранный подход, стоит пройтись по альтернативам — их несколько, и у каждой есть свои trade-off.
Pillow — рисование напрямую на растре. Работает, но верстать в координатах пикселей неудобно: любое изменение дизайна требует менять код. Хрупко и муторно.
HTML + Playwright — по сути тот же подход, что описан ниже, только шаблон в HTML/CSS вместо SVG. Больше гибкости для сложной вёрстки, но SVG точнее контролирует позиционирование и размеры — для карточек фиксированного формата это важно.
Jinja2 + SVG — полноценный шаблонизатор вместо string replace. Оправдано, если в шаблоне нужны условия или циклы. Для простой подстановки значений — избыточно.
cairosvg — рендеринг SVG без браузера, быстрее и легче Playwright. Но плохо справляется с нестандартными шрифтами и встроенными изображениями. Именно поэтому от него отказались.
Внешние SaaS-сервисы — Bannerbear, Placid и подобные. Работают, у них есть API и шаблонный редактор. И ежемесячная подписка. Задача, которая решается локально в 100 строк Python, там стоит денег каждый месяц. Платить имеет смысл, если в команде нет разработчика совсем. Если разработчик есть — это просто ненужная зависимость и операционные расходы, которые никуда не денутся.
SVG как шаблонизатор
SVG — это XML. XML — это текст. А текст можно парсить как строку и подставлять в него что угодно.
Шаблон выглядит так: в SVG-файле в нужных местах вместо реальных данных стоят плейсхолдеры в двойных фигурных скобках — {{PRICE}}, {{DEPARTURE_CITY}}, {{DATE}}. Дальше Python читает файл как строку и делает замену.
def replace_svg_data(svg_content: str, data: dict) -> str:
result = svg_content
result = result.replace("{{PRICE}}", data["price"])
result = result.replace("{{DEPARTURE_CITY}}", data["departure"]["city"])
result = result.replace("{{ARRIVAL_CITY}}", data["arrival"]["city"])
result = result.replace("{{DATE}}", data["departure"]["date"])
return resultНикакой магии. Никакого специального шаблонизатора. Просто string replace.
Рендеринг через Playwright
Готовый SVG нужно превратить в PNG. Для этого используется Playwright — он загружает SVG-контент прямо в браузер и делает скриншот.
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1000, "height": 850},
device_scale_factor=2 # для чёткости на ретина-экранах
)
page = context.new_page()
page.set_content(modified_svg)
screenshot = page.screenshot(type="png", full_page=True)Playwright — решение не самое лёгкое: тянет за собой полноценный браузер. Зато он корректно обрабатывает внешние ресурсы, CSS и шрифты там, где более лёгкие инструменты ломаются.
Поскольку Playwright синхронный, а весь остальной код асинхронный, вызов оборачивается в asyncio.to_thread — чтобы не блокировать event loop.
result = await asyncio.to_thread(
_generate_png_from_svg_sync,
svg_path,
ticket_data
)Шрифты — самая неочевидная часть
Первый рабочий вариант использовал @font-face в SVG с относительными путями к файлам шрифтов:
@font-face {
font-family: 'Inter';
src: url('../fonts/Inter-Regular.ttf') format('truetype');
}Локально работало. В Docker-контейнере — нет. Браузер не находил файлы по относительным путям внутри контейнера, и вместо нужного шрифта рендерился системный дефолт.
Решение оказалось простым: установить нужный шрифт в систему внутри контейнера и убрать @font-face правила из SVG совсем. Браузер находит системный шрифт через fontconfig автоматически. Чтобы шрифт точно загрузился до скриншота, добавляется явное ожидание:
page.evaluate("document.fonts.ready")
page.wait_for_timeout(2000)Выглядит как костыль, но это стандартный способ дождаться загрузки шрифтов в браузере перед тем, как делать скриншот.
Итог
SVG как шаблон, Python для подстановки данных, Playwright для рендеринга. Единственная нетривиальная часть — шрифты в Docker, и там тоже решение простое, просто не очевидное с первого раза.
Скучная технология. Скучная технология работает.
// Python Dev
Другие статьи Python Dev
2026-06-10
Почему LLM не заменит хороший парсер. Кейс с автозапчастями
Ко мне пришёл клиент с задачей, которая на старте выглядит почти наивно простой — настолько, что даже немного подозрительно. Есть каталог: больше 50 000 …
2026-06-01
Зачем в свободное время я решил сделать миллиардное приложение ToDo
Todo-приложение — этакий Hello World в мире программирования. Каждый разработчик хоть раз его делал, обычно в самом начале, когда ещё не знаешь что писать, но …
2026-05-30
Два дня на задачу, которая казалась тривиальной: асинхронность загрузки в Telegram-ботах
Есть класс задач, которые выглядят как пятнадцать минут работы. Потом садишься за них и обнаруживаешь, что дело не в коде — дело в том, как устроена система под …
// Python Projects
Проекты Python Dev
2026-05-28
Робот-коллектор: автоматический обзвон должников
Система автоматического голосового обзвона должников с интеграцией с Google Таблицами, синтезом речи, распознаванием ответов и повторными попытками дозвона.
2026-05-27
Автоматическое ведение сети Telegram-каналов для турагента
Система автоматического ведения 150 Telegram-каналов с подбором туров и авиабилетов, генерацией визуалов и публикацией по расписанию.
2026-04-29
Автоматический протокол звонка: от записи до структурированного документа
Автоматический протокол звонка: от записи до структурированного документа Распределённые команды проводят много времени в звонках. Обсуждают задачи, принимают …
// Contact
Нужна помощь?
Свяжись со мной и я помогу решить проблему
Написать в TelegramОтвечаю в течение рабочего дня (03:00–13:00 GMT)
Или оставьте заявку здесь: