// Python Dev

Generación de imágenes a partir de plantillas: SVG + Python + Playwright

Publicado el 17.06.2026

Una de las tareas en el proyecto de envío de billetes de avión es generar automáticamente tarjetas con las ofertas para publicar en comunidades. Precio, ruta, fecha: los datos cambian, la imagen debe actualizarse según un calendario sin intervención humana.

La tarea parece simple, pero se resuelve de distintas maneras. A menudo — más complicado de lo necesario.

Cómo se puede resolver esta tarea

Antes de explicar el enfoque elegido, vale la pena repasar las alternativas — hay varias, y cada una tiene sus compensaciones.

Pillow — dibujar directamente en el mapa de bits. Funciona, pero maquetar en coordenadas de píxeles es incómodo: cualquier cambio de diseño requiere modificar el código. Frágil y tedioso.

HTML + Playwright — en esencia el mismo enfoque que se describe más abajo, solo que la plantilla es HTML/CSS en lugar de SVG. Más flexibilidad para maquetación compleja, pero SVG controla con más precisión el posicionamiento y los tamaños — para tarjetas de formato fijo esto es importante.

Jinja2 + SVG — un motor de plantillas completo en lugar de reemplazar cadenas. Está justificado si la plantilla necesita condiciones o bucles. Para una sustitución simple de valores — excesivo.

cairosvg — renderizado de SVG sin navegador, más rápido y ligero que Playwright. Pero gestiona mal tipografías no estándar e imágenes embebidas. Por eso se descartó.

Servicios SaaS externos — Bannerbear, Placid y similares. Funcionan, tienen API y un editor de plantillas. Y una suscripción mensual. Una tarea que se resuelve localmente en 100 líneas de Python allí cuesta dinero cada mes. Pagar tiene sentido si en el equipo no hay ningún desarrollador. Si hay un desarrollador — es simplemente una dependencia innecesaria y un gasto operativo que no desaparece.

SVG como plantilla

SVG es XML. XML es texto. Y el texto se puede parsear como cadena y en él se puede sustituir cualquier cosa.

La plantilla se ve así: en el archivo SVG, en los lugares necesarios en vez de los datos reales hay marcadores de posición entre llaves dobles — {{PRICE}}, {{DEPARTURE_CITY}}, {{DATE}}. Luego Python lee el archivo como cadena y realiza la sustitución.

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

No hay magia. No hay un motor de plantillas especial. Simplemente string replace.

Renderizado mediante Playwright

El SVG resultante hay que convertirlo en PNG. Para eso se usa Playwright — carga el contenido SVG directamente en el navegador y hace una captura.

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  # para mayor nitidez en pantallas Retina
    )
    page = context.new_page()
    page.set_content(modified_svg)
    screenshot = page.screenshot(type="png", full_page=True)

Playwright no es la solución más ligera: arrastra un navegador completo. Pero maneja correctamente recursos externos, CSS y fuentes donde herramientas más ligeras fallan.

Dado que Playwright es sincrónico y el resto del código es asíncrono, la llamada se envuelve en asyncio.to_thread — para no bloquear el event loop.

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

Fuentes — la parte menos obvia

La primera versión funcional usaba @font-face en el SVG con rutas relativas a los archivos de fuentes:

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

Funciona localmente. En el contenedor Docker — no. El navegador no encontraba los archivos por rutas relativas dentro del contenedor, y en lugar de la fuente necesaria se renderizaba la fuente por defecto del sistema.

La solución resultó ser simple: instalar la fuente necesaria en el sistema dentro del contenedor y eliminar las reglas @font-face del SVG por completo. El navegador encuentra la fuente del sistema a través de fontconfig automáticamente. Para asegurarse de que la fuente se haya cargado antes de la captura, se añade una espera explícita:

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

Parece un parche, pero es la forma estándar de esperar a que las fuentes se carguen en el navegador antes de hacer la captura de pantalla.

Resultado

SVG como plantilla, Python para la sustitución de datos, Playwright para el renderizado. La única parte no trivial son las fuentes en Docker, y allí la solución también es simple, solo no obvia a primera vista.

Tecnología aburrida. La tecnología aburrida funciona.

// Python Dev

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

Все статьи

// Python Projects

Проекты Python Dev

Все проекты

// Contact

¿Necesitas ayuda?

Escríbeme y te ayudaré a resolver el problema

Escribir en Telegram

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

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

Enviar solicitud
Escribir y recibir una respuesta rápida