What is SVG?
SVG (Scalable Vector Graphics) is an XML-based image format that describes graphics using mathematical equations rather than pixels. This means SVG images scale to any size without quality loss — from a 16×16 px favicon to a 3-meter billboard, SVG stays perfectly sharp.
Developed by the W3C and natively supported in all modern browsers, SVG is the standard for:
- Logos and icons — no pixelation on Retina/4K screens
- Interactive illustrations — native CSS/JavaScript animations
- Infographics — data visualizations that scale responsively
- Scientific charts — matplotlib can export SVG directly
- Web maps — D3.js, Leaflet and Mapbox all use SVG
An SVG file is readable XML text, editable in any code editor.
Internal SVG Structure
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="200" height="200"
viewBox="0 0 200 200">
<!-- Reusable definitions -->
<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>
<!-- Basic shapes -->
<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"/>
<!-- Text -->
<text x="100" y="108" text-anchor="middle"
font-family="Arial" font-size="24" fill="#333">
SVG
</text>
</svg>
Installing Python Libraries
pip install svgwrite lxml cairosvg Pillow
For Inkscape conversion (optional):
# Ubuntu/Debian
sudo apt install inkscape
# macOS
brew install inkscape
# Windows — download from inkscape.org
Creating SVG with svgwrite
svgwrite is the most Pythonic library for generating SVGs programmatically:
import svgwrite
def create_bar_chart(data, filename='chart.svg'):
"""Generate an SVG bar chart."""
width, height = 600, 400
margin = {'left': 60, 'right': 20, 'top': 30, 'bottom': 60}
dwg = svgwrite.Drawing(filename, size=(width, height))
# Background
dwg.add(dwg.rect((0, 0), (width, height), fill='#f8f9fa'))
# Chart area dimensions
chart_w = width - margin['left'] - margin['right']
chart_h = height - margin['top'] - margin['bottom']
max_val = max(v for _, v in data)
# Grid lines
for i in range(5):
y = margin['top'] + i * (chart_h / 4)
dwg.add(dwg.line(
(margin['left'], y), (width - margin['right'], y),
stroke='#dee2e6', stroke_width=1
))
# Bars
bar_w = chart_w / len(data) * 0.7
spacing = chart_w / len(data)
colors = ['#4361ee', '#3a0ca3', '#7209b7', '#f72585', '#4cc9f0']
for i, (label, value) in enumerate(data):
x = margin['left'] + i * spacing + spacing * 0.15
h = (value / max_val) * chart_h
y = margin['top'] + chart_h - h
# Bar
dwg.add(dwg.rect(
(x, y), (bar_w, h),
fill=colors[i % len(colors)],
rx=3, ry=3
))
# Value label above bar
dwg.add(dwg.text(
str(value),
insert=(x + bar_w / 2, y - 5),
text_anchor='middle',
font_size='12px',
font_family='Arial',
fill='#333'
))
# X-axis label
dwg.add(dwg.text(
label,
insert=(x + bar_w / 2, height - margin['bottom'] + 20),
text_anchor='middle',
font_size='11px',
font_family='Arial',
fill='#555'
))
dwg.save()
print(f"SVG saved: {filename}")
# Example
monthly_data = [
('Jan', 120), ('Feb', 185), ('Mar', 142),
('Apr', 210), ('May', 167), ('Jun', 230)
]
create_bar_chart(monthly_data)
Editing SVG with lxml
For modifying existing SVGs (changing colors, text, attributes):
from lxml import etree
def replace_svg_colors(input_path, output_path, color_map):
"""
Replace colors in an SVG.
color_map = {'#ff0000': '#0000ff', ...}
"""
tree = etree.parse(input_path)
root = tree.getroot()
for elem in root.iter():
for attr in ('fill', 'stroke'):
val = elem.get(attr)
if val and val.lower() in color_map:
elem.set(attr, color_map[val.lower()])
# Also handle inline style attribute
style = elem.get('style', '')
for old, new in color_map.items():
style = style.replace(old, new)
if style:
elem.set('style', style)
tree.write(output_path, pretty_print=True,
xml_declaration=True, encoding='UTF-8')
print(f"Recolored SVG written: {output_path}")
def extract_svg_texts(path):
"""Extract all text elements from an SVG."""
tree = etree.parse(path)
root = tree.getroot()
texts = []
for elem in root.iter('{http://www.w3.org/2000/svg}text'):
text = ''.join(elem.itertext()).strip()
if text:
texts.append(text)
return texts
# Usage
replace_svg_colors(
'logo_original.svg',
'logo_dark.svg',
{'#ffffff': '#1a1a2e', '#000000': '#e0e0e0'}
)
Converting SVG to PNG/PDF with CairoSVG
CairoSVG is the best pure-Python solution for rasterizing SVGs:
import cairosvg
import os
def svg_to_png(svg_input, png_output, scale=2.0, width=None, height=None):
"""
Convert SVG to high-resolution PNG.
scale=2.0 → double resolution (ideal for Retina displays)
"""
kwargs = {'scale': scale}
if width:
kwargs['output_width'] = width
if height:
kwargs['output_height'] = height
cairosvg.svg2png(url=svg_input, write_to=png_output, **kwargs)
size_kb = os.path.getsize(png_output) / 1024
print(f"PNG generated: {png_output} ({size_kb:.1f} KB)")
def svg_to_pdf(svg_input, pdf_output):
"""Convert SVG to vector PDF (preserves scalability for print)."""
cairosvg.svg2pdf(url=svg_input, write_to=pdf_output)
print(f"PDF generated: {pdf_output}")
def batch_svg_to_png(folder, scale=2.0):
"""Convert all SVGs in a folder to PNG."""
from pathlib import Path
svgs = list(Path(folder).glob('*.svg'))
print(f"Processing {len(svgs)} SVG files...")
for svg in svgs:
png = svg.with_suffix('.png')
try:
svg_to_png(str(svg), str(png), scale=scale)
except Exception as e:
print(f" Error in {svg.name}: {e}")
print("Batch conversion complete.")
# Examples
svg_to_png('logo.svg', 'logo_2x.png', scale=2.0) # Retina
svg_to_png('logo.svg', 'logo_512.png', width=512) # Fixed size
svg_to_png('icon.svg', 'icon_192.png', width=192) # PWA icon
svg_to_pdf('infographic.svg', 'infographic_print.pdf') # Print
batch_svg_to_png('icons/', scale=3.0) # 3× batch
Inkscape CLI for High-Fidelity Export
For complex conversions with custom fonts:
import subprocess
import shutil
def inkscape_svg_to_png(svg, output, width=None, dpi=96):
"""Use Inkscape for pixel-perfect rendering with custom fonts."""
if not shutil.which('inkscape'):
raise RuntimeError("Inkscape is not installed")
cmd = ['inkscape', svg, f'--export-filename={output}']
if width:
cmd.append(f'--export-width={width}')
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 exported with Inkscape: {output}")
Exporting SVG from matplotlib
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('SVG')
def export_chart_svg(x_data, y_data, title, filename):
"""Export a matplotlib chart as high-quality SVG."""
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x_data, y_data, linewidth=2, color='#4361ee',
marker='o', markersize=6, markerfacecolor='#f72585')
ax.fill_between(x_data, y_data, alpha=0.1, color='#4361ee')
ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
ax.set_xlabel('Month', fontsize=12)
ax.set_ylabel('Value', fontsize=12)
ax.grid(True, alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
fig.savefig(filename, format='svg', bbox_inches='tight',
transparent=True, dpi=150)
plt.close(fig)
print(f"Chart SVG: {filename}")
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
sales = [120, 185, 142, 210, 167, 230]
export_chart_svg(months, sales, 'Sales 2024', 'sales.svg')
Animated SVG
def create_animated_loader(output='loader.svg'):
"""Generate an SVG with pure CSS animation."""
content = '''<?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>
.ring {
transform-origin: 40px 40px;
animation: spin 1.2s linear infinite;
}
.ring:nth-child(2) { animation-delay: -0.4s; opacity: 0.75; }
.ring:nth-child(3) { animation-delay: -0.8s; opacity: 0.5; }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<circle class="ring" cx="40" cy="40" r="28"
fill="none" stroke="#4361ee" stroke-width="6"
stroke-dasharray="44 132"/>
<circle class="ring" cx="40" cy="40" r="20"
fill="none" stroke="#f72585" stroke-width="5"
stroke-dasharray="31 95"/>
<circle class="ring" cx="40" cy="40" r="12"
fill="none" stroke="#4cc9f0" stroke-width="4"
stroke-dasharray="19 57"/>
</svg>'''
with open(output, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Animated SVG created: {output}")
SVG vs Raster Format Comparison
| Criterion | SVG | PNG/JPG/WebP |
|---|---|---|
| Logos & icons | ✅ Ideal | ❌ Pixelated on Retina |
| Photographs | ❌ Not suitable | ✅ Ideal |
| Simple illustrations | ✅ Very small | ⚠️ Larger |
| Complex artwork | ❌ Huge file | ✅ Compressed |
| Web animations | ✅ Native CSS | ⚠️ GIF/APNG |
| Professional print | ✅ Vector | ⚠️ Requires high DPI |
| Email support | ⚠️ Limited | ✅ Universal |
SVG Cleaning and Optimization
import re
def clean_svg(content):
"""Strip unnecessary metadata from exported SVGs."""
# Remove Inkscape/Illustrator comments
content = re.sub(r'<!--.*?-->', '', content, flags=re.DOTALL)
# Remove Inkscape namespaces
content = re.sub(r'\s+xmlns:(inkscape|sodipodi|dc|cc|rdf)="[^"]*"', '', content)
# Remove metadata blocks
content = re.sub(r'<metadata>.*?</metadata>', '', content, flags=re.DOTALL)
# Collapse blank lines
content = re.sub(r'\n\s*\n', '\n', content)
return content.strip()
# Typical reduction: 20-40% for Illustrator/Inkscape exports
Conclusion
SVG is the de facto vector format for the modern web. Python offers a mature ecosystem for generating it (svgwrite), manipulating it (lxml), and converting to raster (cairosvg). The right tool for the right job: svgwrite for generating from scratch, lxml for editing existing files, cairosvg for exporting to PNG/PDF, and Inkscape CLI for maximum typographic fidelity.
For production projects, always pair programmatic generation with an optimization step (svgo) to cut file size by 20–60%.
Related conversions
Most teams that read this guide convert images in one of these directions: