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.
Before & After
| Before (text elements) | After (vector paths) |
|---|---|
| Requires fonts on viewer's system | Renders identically everywhere |
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)
- Auto font download - Missing fonts downloaded automatically via fontget/fnt
- SVG validation - Input/output validation via svg-matrix
- Multi-format input - SVG files, HTML, CSS, URLs, Python/JS code, Markdown, RST, ePub
- Visual diff tools - Pixel-perfect comparison via svg-bbox
- Offline mode - Graceful degradation when network is unavailable
- Cross-platform - Works on macOS, Linux, and Windows
Installation
Requires Python 3.11+ and uv (fast Python package manager).
Install uv
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Or via Homebrew
brew install uv
# Or via pip (if you have Python)
pip install uv
After installation, restart your terminal or run source ~/.bashrc (Linux) / source ~/.zshrc (macOS).
Three 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
# Convert large files that exceed default size limits
text2path convert large_file.svgz -o output.svg --no-size-limit
# Auto-download missing fonts (requires fontget or fnt)
text2path convert input.svg -o output.svg --auto-download
# Validate SVG structure (requires Bun)
text2path convert input.svg -o output.svg --validate
# Combine validation and auto-download
text2path convert input.svg -o output.svg --auto-download --validate
Supported Input Formats
All formats are auto-detected. Use --format <type> to force a specific format.
| Format | Extension | Example Command | Description |
|---|---|---|---|
| SVG | .svg |
text2path convert input.svg -o output.svg |
Standard SVG files |
| SVGZ | .svgz |
text2path convert compressed.svgz -o out.svgz |
Gzip-compressed SVG |
| Inkscape | .svg |
text2path convert inkscape.svg -o out.svg |
Preserves sodipodi/inkscape metadata |
| HTML | .html |
text2path convert page.html -o page_out.html |
Inline SVG, <object>, <img>, data URIs |
| CSS | .css |
text2path convert styles.css -o styles_out.css |
SVG in url() data URIs |
| Python | .py |
text2path convert icons.py -o icons_out.py |
SVG strings in Python code |
| JavaScript | .js, .ts, .jsx, .tsx |
text2path convert app.tsx -o app_out.tsx |
SVG in template literals and strings |
| Markdown | .md |
text2path convert README.md -o README_out.md |
Embedded SVG (HTML blocks, data URIs) |
| RST | .rst |
text2path convert docs.rst -o docs_out.rst |
reStructuredText .. raw:: directives |
| Plain Text | .txt |
text2path convert base64.txt -o base64_out.txt |
Base64-encoded SVG content |
| ePub | .epub |
text2path convert book.epub -o book_out.epub |
ePub v3+ ebooks with embedded SVG |
| Remote URL | https:// |
text2path convert "https://example.com/icon.svg" -o icon.svg |
Fetch and convert remote SVG |
| Data URI | data: |
text2path convert 'data:image/svg+xml;base64,PHN2Zy...' -o out.svg |
Base64 or URL-encoded SVG |
| CSS URL | url() |
text2path convert 'url("data:image/svg+xml;base64,...")' -o out.txt |
CSS url() wrapped data URI |
Additional Commands
# Force specific input format
text2path convert input --format html -o output.html
# Output as base64 data URI
text2path convert input.svg --base64 -o encoded.txt
# Batch convert with YAML config (see "Batch Processing" section)
text2path batch template config.yaml && text2path batch convert config.yaml
# 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
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
For simple batch operations, use glob patterns:
# Quick batch with glob pattern
for f in assets/*.svg; do text2path convert "$f" -o "dist/${f%.svg}_paths.svg"; done
For complex workflows with multiple folders, verification, and reporting, use YAML configuration (see Batch Processing below).
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
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"
# Security settings for file size limits
security:
ignore_size_limits: false # Bypass all size limits (WARNING: security risk)
max_file_size_mb: 50 # Maximum input file size in MB
max_decompressed_size_mb: 100 # Maximum decompressed size for .svgz/.epub
Environment Variables
# Custom font cache location
export T2P_FONT_CACHE=/path/to/font_cache.json
# Verbose logging
export TEXT2PATH_LOG_LEVEL=DEBUG
# Security settings
export TEXT2PATH_IGNORE_SIZE_LIMITS=true # Bypass file size limits (use with caution)
export TEXT2PATH_MAX_FILE_SIZE_MB=100 # Custom max file size (default: 50)
export TEXT2PATH_MAX_DECOMPRESSED_SIZE_MB=200 # Custom max decompressed size (default: 100)
Batch Processing
For converting multiple files with advanced options like verification, parallel processing, and detailed logging, use YAML-based batch configuration.
Quick Start
# 1. Generate a configuration template
text2path batch template my_batch.yaml
# 2. Edit the template (follow the comments inside)
# - Configure the 'inputs' section (required)
# - Adjust settings if needed (optional)
# 3. Run the batch conversion
text2path batch convert my_batch.yaml
Template Structure
The generated template is extensively documented with:
- All available settings with their default values
- Clear explanations of each option
- Example configurations for common scenarios
- Tips for troubleshooting
Don't duplicate the template documentation here - generate the template and read the comments inside:
text2path batch template --help # See template command options
text2path batch template config.yaml # Generate the template
Example Workflow
# Generate template
text2path batch template project_batch.yaml
# Edit: uncomment and modify the inputs section
# (the template has examples for both folder and file modes)
# Run batch conversion
text2path batch convert project_batch.yaml
# Check the log for results
cat batch_conversion_log.json | jq '.summary'
# Output: { "total": 42, "success": 40, "skipped": 2, "errors": 0 }
Available Commands
# Generate configuration template
text2path batch template [output.yaml] # Default: batch_config.yaml
text2path batch template config.yaml -f # --force to overwrite
# Run batch conversion
text2path batch convert config.yaml
# Compare converted files against originals
text2path batch compare --samples-dir ./samples --threshold 0.5
# Track conversion quality over time
text2path batch regression --registry ./history.json
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
auto_download_fonts=False, # Auto-download missing fonts via fontget/fnt
validate_svg=False, # Validate input/output SVG via svg-matrix
)
# Methods
result = converter.convert_file(input_path, output_path)
output_svg = converter.convert_string(svg_content)
output_svg, result = converter.convert_string(svg_content, return_result=True)
element = converter.convert_element(text_element)
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
missing_fonts: list[str] # Fonts that couldn't be resolved
input_valid: bool | None # Input SVG validation result (if --validate)
output_valid: bool | None # Output SVG validation result (if --validate)
validation_issues: list[str] # SVG validation issues found
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 | Interface | Example |
|---|---|---|
| SVG file | CLI + API | text2path convert input.svg |
| SVG string | API only | converter.convert_string("<svg>...</svg>") |
| ElementTree | API only | converter.convert_tree(ET.parse("file.svg")) |
| Inkscape SVG | CLI + API | Works with sodipodi namespace files |
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 --refresh
# 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")
Offline Mode
When running without network connectivity (e.g., in containers or air-gapped environments):
- Font auto-download: Skipped with message "Cannot download: no network (offline)"
- SVG validation: Skipped with message "Validation skipped (offline mode)"
The conversion itself works fully offline - only the optional features require network.
# Works offline - core conversion doesn't need network
converter = Text2PathConverter()
result = converter.convert_file("input.svg", "output.svg")
# These features gracefully degrade when offline:
converter = Text2PathConverter(
auto_download_fonts=True, # Skips download if no network
validate_svg=True, # Skips validation if no network
)
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 |
| Bun | SVG validation via svg-matrix | curl -fsSL https://bun.sh/install | sh |
| fontget | Auto font download | curl -fsSL https://raw.githubusercontent.com/Graphixa/FontGet/main/scripts/install.sh | sh |
| fnt | Auto font download (fallback) | brew install fnt |
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
- Decompression Bomb Protection: Size limits on compressed files (.svgz, .epub)
- Default max file size: 50MB
- Default max decompressed size: 100MB
- Can be customized via config or overridden with
--no-size-limit(use with caution)
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.4.7.tar.gz.
File metadata
- Download URL: svg_text2path-0.4.7.tar.gz
- Upload date:
- Size: 127.9 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 |
4a2f132623a09368844e027ef35c589c5ec1b06d9c171fab5e6ece0407a8a149
|
|
| MD5 |
37be7cf217ca0e22ad1297bb4f1314ce
|
|
| BLAKE2b-256 |
7e8f6d57a4152bcc6c9edf8a48d49a3e67f9e4ca7cb98041fc896e7e5547e5bc
|
File details
Details for the file svg_text2path-0.4.7-py3-none-any.whl.
File metadata
- Download URL: svg_text2path-0.4.7-py3-none-any.whl
- Upload date:
- Size: 159.1 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 |
1c82f896f05306b60730328ebdbd59e95a0dd26f86350426e6eaa351b7697552
|
|
| MD5 |
b451db2867b858e0e0119e6f70b86916
|
|
| BLAKE2b-256 |
3b4a841f22139158e00b41cf64c0783bd1459dbdaf86931ba734d1cfcd4b8aff
|