Convert SVG text elements to path outlines with HarfBuzz shaping
Project description
svg-text2path
Convert SVG text elements (<text>, <tspan>, <textPath>) to vector outline paths with HarfBuzz text shaping.
Why svg-text2path?
When you embed text in SVG files, the viewer must have the correct fonts installed to render them properly. This causes problems when:
- Sharing SVGs across different systems with different fonts
- Converting SVGs to other formats (PDF, PNG) where font embedding is unreliable
- Creating SVG icons or logos that must look identical everywhere
- Archiving designs for long-term preservation
svg-text2path solves this by converting text to vector paths that render identically on any system, without requiring fonts.
Features
- HarfBuzz text shaping - Proper ligatures, kerning, and complex script support
- Unicode BiDi - RTL languages (Arabic, Hebrew) rendered correctly
- TextPath support - Text along paths with tangent-based placement
- Strict font matching - Fails on missing fonts (no silent fallbacks)
- 20+ input formats - File, string, HTML, CSS, JSON, markdown, remote URLs
- Visual diff tools - Pixel-perfect comparison via svg-bbox
- Cross-platform - Works on macOS, Linux, and Windows
Installation
Requires Python 3.11+ (Python 3.11 recommended for best compatibility)
Three installation methods depending on your use case:
1. CLI Tool (End Users)
Install the text2path command globally in an isolated environment:
# Install (with recommended Python version)
uv tool install svg-text2path --python 3.11
# Upgrade to latest version
uv tool install svg-text2path --python 3.11 --upgrade
# Uninstall
uv tool uninstall svg-text2path
The command is available system-wide without activating any virtual environment.
2. Library Dependency (Your Project)
Add svg-text2path as a dependency in your Python project:
# Install (adds to pyproject.toml)
uv add svg-text2path
# Uninstall (removes from pyproject.toml)
uv remove svg-text2path
Use this when you want to import svg_text2path in your code. Make sure your project uses Python 3.11+.
3. Direct Install (Virtual Environment)
Install directly into a virtual environment:
# Create venv with compatible Python version
uv venv --python 3.11
source .venv/bin/activate # or .venv\Scripts\activate on Windows
# Install
uv pip install svg-text2path
# Uninstall
uv pip uninstall svg-text2path
Use this for quick testing or scripts without a pyproject.toml.
Platform-Specific Notes
macOS
Fonts are loaded from /Library/Fonts, /System/Library/Fonts, and ~/Library/Fonts. No additional setup required.
Linux
For best results, install fontconfig:
# Debian/Ubuntu
sudo apt-get install fontconfig
# Fedora/RHEL
sudo dnf install fontconfig
# Arch
sudo pacman -S fontconfig
Windows
Fonts are loaded from C:\Windows\Fonts and the user font directory. For enhanced font matching, the library uses Windows font APIs automatically.
Development Setup
git clone https://github.com/Emasoft/svg-text2path.git
cd svg-text2path
# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Sync dependencies (creates venv and installs all deps)
uv sync --all-extras
# Install pre-push hook (runs lint, typecheck, format, tests before push)
cp scripts/pre-push.sh .git/hooks/pre-push
chmod +x .git/hooks/pre-push
# Run tests
uv run pytest tests/ -v
# Build package
uv build
# Publish to PyPI (requires PyPI token)
uv publish
Quick Start
Python Library
from svg_text2path import Text2PathConverter
converter = Text2PathConverter()
# Convert a file
result = converter.convert_file("input.svg", "output.svg")
print(f"Converted {result.text_count} text elements to {result.path_count} paths")
# Convert an SVG string
svg_content = '''<svg xmlns="http://www.w3.org/2000/svg" width="200" height="50">
<text x="10" y="35" font-family="Arial" font-size="24">Hello World</text>
</svg>'''
output = converter.convert_string(svg_content)
# Check for errors
if result.errors:
for error in result.errors:
print(f"Error: {error}")
Command Line
# Basic conversion
text2path convert input.svg -o output.svg
# Convert with higher precision (more decimal places in paths)
text2path convert input.svg -o output.svg --precision 8
# Batch convert multiple files
text2path batch convert *.svg --output-dir ./converted/
# Compare original and converted visually
text2path compare original.svg converted.svg --threshold 0.5
# Pixel-perfect comparison with diff image
text2path compare original.svg converted.svg --pixel-perfect --generate-diff
# List available fonts
text2path fonts list
# Find a specific font
text2path fonts find "Noto Sans"
# Generate font report for an SVG
text2path fonts report input.svg --detailed
# Check external dependencies
text2path deps check
Use Cases
1. Creating Font-Independent Logos
from svg_text2path import Text2PathConverter
converter = Text2PathConverter(precision=6)
result = converter.convert_file("logo_with_text.svg", "logo_paths.svg")
# The output SVG will render identically on any system
2. Batch Processing Design Assets
# Convert all SVGs in a directory
text2path batch convert assets/*.svg --output-dir dist/
# Compare against reference renders
text2path batch compare --samples-dir ./reference --threshold 0.3
3. Verifying Conversion Quality
from svg_text2path import Text2PathConverter
from svg_text2path.tools.visual_comparison import ImageComparator
# Convert
converter = Text2PathConverter()
converter.convert_file("input.svg", "output.svg")
# Compare pixel-by-pixel
comparator = ImageComparator()
diff_percent = comparator.compare("input.svg", "output.svg")
print(f"Visual difference: {diff_percent:.2f}%")
4. Working with Complex Scripts
from svg_text2path import Text2PathConverter
converter = Text2PathConverter()
# Arabic text (RTL with complex shaping)
arabic_svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="300" height="50">
<text x="280" y="35" font-family="Noto Naskh Arabic" font-size="24"
text-anchor="end" direction="rtl">مرحبا بالعالم</text>
</svg>'''
result = converter.convert_string(arabic_svg)
# HarfBuzz handles proper glyph shaping and BiDi text direction
5. Processing Remote SVGs
from svg_text2path import Text2PathConverter
converter = Text2PathConverter()
# Fetch and convert remote SVG (with SSRF protection)
result = converter.convert_url(
"https://example.com/diagram.svg",
"local_output.svg"
)
Configuration
YAML Config File
Create ~/.text2path/config.yaml or ./text2path.yaml:
defaults:
precision: 6 # Decimal places for path coordinates
preserve_styles: false # Keep style attributes on converted paths
output_suffix: "_paths" # Suffix for output files
fonts:
system_only: false # Only use system fonts (ignore custom dirs)
custom_dirs:
- ~/.fonts/custom
- /opt/fonts/brand
# Font family replacements (useful for cross-platform consistency)
replacements:
"Arial": "Liberation Sans"
"Helvetica": "Liberation Sans"
"Times New Roman": "Liberation Serif"
Environment Variables
# Custom font cache location
export T2P_FONT_CACHE=/path/to/font_cache.json
# Verbose logging
export T2P_LOG_LEVEL=DEBUG
API Reference
Text2PathConverter
from svg_text2path import Text2PathConverter
converter = Text2PathConverter(
font_cache=None, # Optional: reuse FontCache across calls
precision=6, # Path coordinate precision (1-12)
preserve_styles=False, # Keep style metadata on paths
log_level="WARNING", # Logging level
)
# Methods
result = converter.convert_file(input_path, output_path)
result = converter.convert_string(svg_content)
element = converter.convert_element(text_element)
result = converter.convert_url(url, output_path)
ConversionResult
from dataclasses import dataclass
from pathlib import Path
from xml.etree.ElementTree import Element
@dataclass
class ConversionResult:
success: bool # True if conversion completed
input_format: str # Detected input format
output: Path | str | Element # Output location or content
errors: list[str] # Error messages
warnings: list[str] # Warning messages
text_count: int # Number of text elements found
path_count: int # Number of paths generated
FontCache
from svg_text2path import FontCache
cache = FontCache()
cache.prewarm() # Build font cache (run once)
# Get font for specific parameters
font, data, face_idx = cache.get_font(
family="Arial",
weight=400, # 100-900
style="normal", # normal, italic, oblique
stretch="normal" # condensed, normal, expanded
)
Supported Input Formats
| Format | Detection | Example |
|---|---|---|
| SVG file | .svg extension |
input.svg |
| SVGZ (compressed) | .svgz or gzip magic |
input.svgz |
| SVG string | Starts with <svg or <text |
"<svg>...</svg>" |
| ElementTree | isinstance(x, Element) |
ET.parse("file.svg") |
| HTML with SVG | Contains <svg tag |
"<html>...<svg>...</svg></html>" |
| CSS data URI | url("data:image/svg+xml |
CSS background image |
| Inkscape SVG | sodipodi namespace | Inkscape-exported files |
| Remote URL | http:// or https:// |
"https://example.com/file.svg" |
Troubleshooting
"Font not found" Error
FontNotFoundError: Font not found: CustomFont (weight=400, style=normal)
Solutions:
- Install the missing font using FontGet or fnt:
fontget install "Noto Sans" # or fnt install "Noto Sans"
- Use a font replacement in config:
replacements: "CustomFont": "Arial"
- Check available fonts:
text2path fonts list
Visual Differences After Conversion
Small differences (< 1%) are normal due to:
- Anti-aliasing differences between text and path rendering
- Sub-pixel positioning variations
- Hinting differences
For pixel-perfect comparison:
text2path compare original.svg converted.svg --pixel-perfect --tolerance 5
Slow Performance with Many Fonts
The first run builds a font cache. Speed up subsequent runs:
# Pre-warm the cache
text2path fonts cache --rebuild
# Or set a custom cache location
export T2P_FONT_CACHE=/fast/disk/font_cache.json
Windows Path Issues
Ensure paths use forward slashes or raw strings:
# Correct
converter.convert_file("C:/Users/name/input.svg", "output.svg")
converter.convert_file(r"C:\Users\name\input.svg", "output.svg")
# Incorrect (escape issues)
converter.convert_file("C:\Users\name\input.svg", "output.svg")
Requirements
Python Dependencies
| Package | Purpose |
|---|---|
fonttools |
Font parsing, glyph extraction |
uharfbuzz |
HarfBuzz text shaping |
python-bidi |
Unicode BiDi algorithm |
defusedxml |
XXE-safe XML parsing |
click |
CLI framework |
rich |
Terminal formatting |
pillow |
Image processing |
numpy |
Array operations |
External Tools (Optional)
| Tool | Purpose | Install |
|---|---|---|
| fontconfig | Enhanced font matching | apt install fontconfig |
| Node.js | Chrome-based comparison | brew install node |
| Inkscape | Reference rendering | apt install inkscape |
Font Installation Tools (Recommended)
Need more fonts? These tools make installing fonts easy:
| Tool | Description | Link |
|---|---|---|
| FontGet | Download and install 2700+ Google Fonts with a simple CLI | github.com/Graphixa/FontGet |
| fnt | Lightweight font manager for Linux/macOS, downloads from Google Fonts | github.com/alexmyczko/fnt |
# FontGet - install any Google Font
fontget install "Noto Sans"
fontget install "Roboto Mono"
# fnt - browse and install fonts
fnt update # Update font list
fnt search noto # Search for fonts
fnt preview "Noto Sans" # Preview a font
fnt install "Noto Sans" # Install a font
Security
- XXE Protection: All XML parsing uses
defusedxml - SSRF Protection: Remote URL fetching blocks private IP ranges (10.x, 172.16.x, 192.168.x, 127.x)
- Input Validation: File paths are validated before processing
License
MIT
Contributing
See CONTRIBUTING.md for development setup and guidelines.
Changelog
See CHANGELOG.md for version history.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file svg_text2path-0.3.4.tar.gz.
File metadata
- Download URL: svg_text2path-0.3.4.tar.gz
- Upload date:
- Size: 150.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22430c488ac401b6cdb0e23e52501b533b96602dfd9b976b1bcd3115f8948cfb
|
|
| MD5 |
dc87a29ffed7ed524de42b590a6664e5
|
|
| BLAKE2b-256 |
d44f59398ce4bd2c7244a28b08ebd82ce5a1fd08265d5e4570916bff9888880c
|
File details
Details for the file svg_text2path-0.3.4-py3-none-any.whl.
File metadata
- Download URL: svg_text2path-0.3.4-py3-none-any.whl
- Upload date:
- Size: 185.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d613d58a2fcc343933d8c67e236bae0c54801c8d71d4aeea32d6cc04aea21c9b
|
|
| MD5 |
3377730f92e82dcd81bcb0cfd201816a
|
|
| BLAKE2b-256 |
78a1ad0158f85240ae5de0dfcc98433eaecfcdf5dad41e32cb3560f84297b91d
|