Skip to main content

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.

CI PyPI Python License: MIT

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)
Before After
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:

  1. Install the missing font using FontGet or fnt:
    fontget install "Noto Sans"  # or
    fnt install "Noto Sans"
    
  2. Use a font replacement in config:
    replacements:
      "CustomFont": "Arial"
    
  3. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

svg_text2path-0.4.5.tar.gz (125.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

svg_text2path-0.4.5-py3-none-any.whl (157.1 kB view details)

Uploaded Python 3

File details

Details for the file svg_text2path-0.4.5.tar.gz.

File metadata

  • Download URL: svg_text2path-0.4.5.tar.gz
  • Upload date:
  • Size: 125.6 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

Hashes for svg_text2path-0.4.5.tar.gz
Algorithm Hash digest
SHA256 bb4a5b2c0ccebd9f1f1f5065bcf7527556164eee1782fce89fb1dad39cd9fc03
MD5 30b7bc59f17f8392f583a3ee911f93e2
BLAKE2b-256 61d1f4752ee5d4973b816d111b1fee7bdba3f72257dcae273a9e1ff3e9345b13

See more details on using hashes here.

File details

Details for the file svg_text2path-0.4.5-py3-none-any.whl.

File metadata

  • Download URL: svg_text2path-0.4.5-py3-none-any.whl
  • Upload date:
  • Size: 157.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

Hashes for svg_text2path-0.4.5-py3-none-any.whl
Algorithm Hash digest
SHA256 34aa025a3ab13cd9b4d44c75fbbc39161aba5e6e4b1e1757fa0de66d36ebf593
MD5 a1c67a05e1caf69a4aa917f1ea5aa916
BLAKE2b-256 2bd701b533ff2aa6337f087f33ec5f5ec858a6d1a6b50e5184619d1bb5825b49

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page