Qué es el formato SVG
SVG (Scalable Vector Graphics) es un formato de imagen basado en XML que describe gráficos mediante ecuaciones matemáticas en lugar de píxeles. Esto permite escalar las imágenes a cualquier tamaño sin perder calidad: desde un icono de 16×16 px hasta una valla publicitaria de 3 metros, el SVG se ve igual de nítido.
Desarrollado por el W3C y soportado de forma nativa en todos los navegadores modernos, SVG es el estándar para:
- Logotipos e iconos — sin pixelado en pantallas Retina/4K
- Ilustraciones interactivas — animaciones CSS/JavaScript nativas
- Infografías — datos visualizados que escalan en responsive
- Gráficas científicas — matplotlib puede exportar SVG directamente
- Mapas web — D3.js, Leaflet y Mapbox usan SVG
Un archivo SVG es texto XML legible y editable en cualquier editor de código.
Estructura interna de un SVG
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="200" height="200"
viewBox="0 0 200 200">
<!-- Definiciones reutilizables -->
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FF6B35;stop-opacity:1"/>
<stop offset="100%" style="stop-color:#F7C59F;stop-opacity:1"/>
</linearGradient>
</defs>
<!-- Formas básicas -->
<rect x="10" y="10" width="180" height="180" rx="15"
fill="url(#grad1)" stroke="#333" stroke-width="2"/>
<circle cx="100" cy="100" r="50" fill="white" opacity="0.8"/>
<!-- Texto -->
<text x="100" y="108" text-anchor="middle"
font-family="Arial" font-size="24" fill="#333">
SVG
</text>
</svg>
Instalación de bibliotecas Python
pip install svgwrite lxml cairosvg Pillow
Para conversión con Inkscape (opcional):
# Ubuntu/Debian
sudo apt install inkscape
# macOS
brew install inkscape
# Windows — descargar desde inkscape.org
Crear SVG con svgwrite
svgwrite es la biblioteca más pythónica para generar SVGs programáticamente:
import svgwrite
def crear_grafica_barras(datos, archivo='grafica.svg'):
"""Genera una gráfica de barras en SVG."""
ancho, alto = 600, 400
margen = {'izq': 60, 'der': 20, 'sup': 30, 'inf': 60}
dwg = svgwrite.Drawing(archivo, size=(ancho, alto))
# Fondo
dwg.add(dwg.rect((0, 0), (ancho, alto), fill='#f8f9fa'))
# Dimensiones del área de gráfica
area_ancho = ancho - margen['izq'] - margen['der']
area_alto = alto - margen['sup'] - margen['inf']
max_val = max(v for _, v in datos)
# Líneas de cuadrícula
for i in range(5):
y = margen['sup'] + i * (area_alto / 4)
dwg.add(dwg.line(
(margen['izq'], y), (ancho - margen['der'], y),
stroke='#dee2e6', stroke_width=1
))
# Barras
barra_ancho = area_ancho / len(datos) * 0.7
espaciado = area_ancho / len(datos)
colores = ['#4361ee', '#3a0ca3', '#7209b7', '#f72585', '#4cc9f0']
for i, (etiqueta, valor) in enumerate(datos):
x = margen['izq'] + i * espaciado + espaciado * 0.15
h = (valor / max_val) * area_alto
y = margen['sup'] + area_alto - h
# Barra con gradiente
dwg.add(dwg.rect(
(x, y), (barra_ancho, h),
fill=colores[i % len(colores)],
rx=3, ry=3
))
# Valor encima
dwg.add(dwg.text(
str(valor),
insert=(x + barra_ancho / 2, y - 5),
text_anchor='middle',
font_size='12px',
font_family='Arial',
fill='#333'
))
# Etiqueta debajo
dwg.add(dwg.text(
etiqueta,
insert=(x + barra_ancho / 2, alto - margen['inf'] + 20),
text_anchor='middle',
font_size='11px',
font_family='Arial',
fill='#555'
))
dwg.save()
print(f"SVG guardado: {archivo}")
# Ejemplo
ventas = [
('Ene', 120), ('Feb', 185), ('Mar', 142),
('Abr', 210), ('May', 167), ('Jun', 230)
]
crear_grafica_barras(ventas)
Manipular SVG con lxml
Para editar SVGs existentes (cambiar colores, textos, atributos):
from lxml import etree
NS = {'svg': 'http://www.w3.org/2000/svg'}
def cambiar_colores_svg(entrada, salida, mapa_colores):
"""
Reemplaza colores en un SVG.
mapa_colores = {'#ff0000': '#0000ff', ...}
"""
arbol = etree.parse(entrada)
raiz = arbol.getroot()
# Recorrer todos los elementos
for elem in raiz.iter():
for attr in ('fill', 'stroke'):
val = elem.get(attr)
if val and val.lower() in mapa_colores:
elem.set(attr, mapa_colores[val.lower()])
# También buscar en el atributo style
style = elem.get('style', '')
for viejo, nuevo in mapa_colores.items():
style = style.replace(viejo, nuevo)
if style:
elem.set('style', style)
arbol.write(salida, pretty_print=True,
xml_declaration=True, encoding='UTF-8')
print(f"SVG recoloreado: {salida}")
def extraer_textos_svg(ruta):
"""Extrae todos los textos de un SVG."""
arbol = etree.parse(ruta)
raiz = arbol.getroot()
textos = []
for elem in raiz.iter('{http://www.w3.org/2000/svg}text'):
texto = ''.join(elem.itertext()).strip()
if texto:
textos.append(texto)
return textos
def escalar_svg(entrada, salida, factor):
"""Escala el viewBox de un SVG por un factor."""
arbol = etree.parse(entrada)
raiz = arbol.getroot()
vb = raiz.get('viewBox', '')
if vb:
partes = [float(x) * factor for x in vb.split()]
raiz.set('viewBox', ' '.join(str(p) for p in partes))
arbol.write(salida, pretty_print=True)
# Uso
cambiar_colores_svg(
'logo_original.svg',
'logo_oscuro.svg',
{'#ffffff': '#1a1a2e', '#000000': '#e0e0e0'}
)
textos = extraer_textos_svg('infografia.svg')
print("Textos encontrados:", textos)
Convertir SVG a PNG/PDF con CairoSVG
CairoSVG es la mejor opción Python pura para rasterizar SVGs:
import cairosvg
import os
def svg_a_png(svg_entrada, png_salida, escala=2.0, ancho=None, alto=None):
"""
Convierte SVG a PNG de alta resolución.
escala=2.0 → doble resolución (ideal para Retina)
"""
kwargs = {'scale': escala}
if ancho:
kwargs['output_width'] = ancho
if alto:
kwargs['output_height'] = alto
cairosvg.svg2png(
url=svg_entrada,
write_to=png_salida,
**kwargs
)
tamaño = os.path.getsize(png_salida) / 1024
print(f"PNG generado: {png_salida} ({tamaño:.1f} KB)")
def svg_a_pdf(svg_entrada, pdf_salida):
"""Convierte SVG a PDF vectorial (conserva escalabilidad)."""
cairosvg.svg2pdf(url=svg_entrada, write_to=pdf_salida)
print(f"PDF generado: {pdf_salida}")
def lote_svg_a_png(carpeta, escala=2.0):
"""Convierte todos los SVGs de una carpeta a PNG."""
from pathlib import Path
svgs = list(Path(carpeta).glob('*.svg'))
print(f"Procesando {len(svgs)} archivos SVG...")
for svg in svgs:
png = svg.with_suffix('.png')
try:
svg_a_png(str(svg), str(png), escala=escala)
except Exception as e:
print(f" Error en {svg.name}: {e}")
print("Conversión en lote completada.")
# Ejemplos
svg_a_png('logo.svg', 'logo_2x.png', escala=2.0) # Retina
svg_a_png('logo.svg', 'logo_512.png', ancho=512) # Tamaño fijo
svg_a_png('icono.svg', 'icono_192.png', ancho=192) # PWA icon
svg_a_pdf('infografia.svg', 'infografia_print.pdf') # Impresión
lote_svg_a_png('iconos/', escala=3.0) # Lote 3×
Convertir con Inkscape CLI
Para conversiones más complejas o con fuentes especiales:
import subprocess
import shutil
def inkscape_svg_a_png(svg, salida, ancho=None, dpi=96):
"""Usa Inkscape para conversión precisa con fuentes."""
if not shutil.which('inkscape'):
raise RuntimeError("Inkscape no está instalado")
cmd = ['inkscape', svg, f'--export-filename={salida}']
if ancho:
cmd.append(f'--export-width={ancho}')
if dpi:
cmd.append(f'--export-dpi={dpi}')
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Inkscape error: {result.stderr}")
print(f"PNG exportado con Inkscape: {salida}")
def optimizar_svg(entrada, salida):
"""Optimiza SVG con svgo (requiere Node.js)."""
cmd = ['npx', 'svgo', entrada, '-o', salida]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
orig = os.path.getsize(entrada)
nuevo = os.path.getsize(salida)
pct = (1 - nuevo / orig) * 100
print(f"Optimizado: {pct:.1f}% de reducción")
return result.returncode == 0
Generar SVG con matplotlib
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('SVG') # Backend SVG
def exportar_grafica_svg(datos_x, datos_y, titulo, archivo):
"""Exporta una gráfica matplotlib como SVG de alta calidad."""
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(datos_x, datos_y, linewidth=2, color='#4361ee',
marker='o', markersize=6, markerfacecolor='#f72585')
ax.fill_between(datos_x, datos_y, alpha=0.1, color='#4361ee')
ax.set_title(titulo, fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('Mes', fontsize=12)
ax.set_ylabel('Valor', fontsize=12)
ax.grid(True, alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
fig.savefig(archivo, format='svg', bbox_inches='tight',
transparent=True, dpi=150)
plt.close(fig)
print(f"Gráfica SVG: {archivo}")
meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun']
ventas = [120, 185, 142, 210, 167, 230]
exportar_grafica_svg(meses, ventas, 'Ventas 2024', 'ventas.svg')
Generar sprites de iconos SVG
def crear_sprite_svg(iconos, salida='sprite.svg'):
"""
Combina múltiples SVGs en un sprite.
iconos = {'nombre': 'ruta/al/icono.svg', ...}
"""
symbols = []
for nombre, ruta in iconos.items():
arbol = etree.parse(ruta)
raiz = arbol.getroot()
vb = raiz.get('viewBox', '0 0 24 24')
# Crear <symbol> con el contenido del SVG
sym = f'<symbol id="{nombre}" viewBox="{vb}">'
for child in raiz:
sym += etree.tostring(child, encoding='unicode')
sym += '</symbol>'
symbols.append(sym)
sprite = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<svg xmlns="http://www.w3.org/2000/svg" style="display:none">\n'
+ '\n'.join(symbols)
+ '\n</svg>'
)
with open(salida, 'w', encoding='utf-8') as f:
f.write(sprite)
print(f"Sprite generado con {len(iconos)} iconos: {salida}")
print("Uso en HTML: <use href='sprite.svg#nombre-icono'/>")
# En HTML:
# <svg width="24" height="24">
# <use href="sprite.svg#icono-home"/>
# </svg>
Animaciones SVG
def crear_svg_animado(salida='loader.svg'):
"""Genera un SVG con animación CSS pura."""
contenido = '''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
<style>
.anillo {
transform-origin: 40px 40px;
animation: girar 1.2s linear infinite;
}
.anillo:nth-child(2) { animation-delay: -0.4s; opacity: 0.75; }
.anillo:nth-child(3) { animation-delay: -0.8s; opacity: 0.5; }
@keyframes girar {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<circle class="anillo" cx="40" cy="40" r="28"
fill="none" stroke="#4361ee" stroke-width="6"
stroke-dasharray="44 132"/>
<circle class="anillo" cx="40" cy="40" r="20"
fill="none" stroke="#f72585" stroke-width="5"
stroke-dasharray="31 95"/>
<circle class="anillo" cx="40" cy="40" r="12"
fill="none" stroke="#4cc9f0" stroke-width="4"
stroke-dasharray="19 57"/>
</svg>'''
with open(salida, 'w', encoding='utf-8') as f:
f.write(contenido)
print(f"SVG animado creado: {salida}")
Cuándo usar SVG vs raster
| Criterio | SVG | PNG/JPG/WebP |
|---|---|---|
| Logotipos e iconos | ✅ Ideal | ❌ Pixelado en Retina |
| Fotografías | ❌ No apto | ✅ Ideal |
| Ilustraciones simples | ✅ Muy pequeño | ⚠️ Más grande |
| Fotografías complejas | ❌ Enorme | ✅ Comprimido |
| Animaciones web | ✅ CSS nativo | ⚠️ GIF/APNG |
| Impresión profesional | ✅ Vectorial | ⚠️ Necesita alta resolución |
| Soporte email | ⚠️ Limitado | ✅ Universal |
Optimización y buenas prácticas
import re
def limpiar_svg(contenido):
"""Elimina metadatos innecesarios de SVGs exportados."""
# Eliminar comentarios de Inkscape/Illustrator
contenido = re.sub(r'<!--.*?-->', '', contenido, flags=re.DOTALL)
# Eliminar namespaces de Inkscape
contenido = re.sub(r'\s+xmlns:(inkscape|sodipodi|dc|cc|rdf)="[^"]*"', '', contenido)
# Eliminar metadata
contenido = re.sub(r'<metadata>.*?</metadata>', '', contenido, flags=re.DOTALL)
# Eliminar líneas vacías
contenido = re.sub(r'\n\s*\n', '\n', contenido)
return contenido.strip()
# Reducción típica: 20-40% en SVGs exportados desde Illustrator/Inkscape
Conclusión
SVG es el formato vectorial de facto para la web moderna. Python ofrece un ecosistema maduro para generarlo (svgwrite), manipularlo (lxml) y convertirlo a raster (cairosvg). La clave para elegir la herramienta correcta: svgwrite para crear desde cero, lxml para editar existentes, cairosvg para exportar a PNG/PDF, e Inkscape CLI para máxima fidelidad tipográfica.
Para proyectos de producción, combina siempre la generación programática con un paso de optimización (svgo) para reducir el tamaño entre un 20-60%.
Conversiones relacionadas
Lo más habitual al trabajar con imágenes son estas direcciones de conversión: