// 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 читает файл как строку и делает замену.

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-контент прямо в браузер и делает скриншот.

python
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.

python
result = await asyncio.to_thread(
    _generate_png_from_svg_sync,
    svg_path,
    ticket_data
)

Шрифты — самая неочевидная часть

Первый рабочий вариант использовал @font-face в SVG с относительными путями к файлам шрифтов:

css
@font-face {
    font-family: 'Inter';
    src: url('../fonts/Inter-Regular.ttf') format('truetype');
}

Локально работало. В Docker-контейнере — нет. Браузер не находил файлы по относительным путям внутри контейнера, и вместо нужного шрифта рендерился системный дефолт.

Решение оказалось простым: установить нужный шрифт в систему внутри контейнера и убрать @font-face правила из SVG совсем. Браузер находит системный шрифт через fontconfig автоматически. Чтобы шрифт точно загрузился до скриншота, добавляется явное ожидание:

python
page.evaluate("document.fonts.ready")
page.wait_for_timeout(2000)

Выглядит как костыль, но это стандартный способ дождаться загрузки шрифтов в браузере перед тем, как делать скриншот.

Итог

SVG как шаблон, Python для подстановки данных, Playwright для рендеринга. Единственная нетривиальная часть — шрифты в Docker, и там тоже решение простое, просто не очевидное с первого раза.

Скучная технология. Скучная технология работает.

// Python Dev

Другие статьи Python Dev

Все статьи

// Python Projects

Проекты Python Dev

Все проекты

// Contact

Нужна помощь?

Свяжись со мной и я помогу решить проблему

Написать в Telegram

Отвечаю в течение рабочего дня (03:00–13:00 GMT)

Или оставьте заявку здесь:

Отправить заявку
Написать и получить быстрый ответ