Skip to main content

VecTeX

Test PyPI Python License

VecTeX compiles TeX source into one self-contained SVG <g> fragment, so equations and labels in a figure are generated from code and stay editable afterwards. Output is deterministic, with stable ids, so a figure can be regenerated and diffed in version control; the fragment is also an editable TexText object once inserted into an Inkscape document.

The time-dependent Schrodinger equation typeset by TeX as an SVG fragment

One display equation, compiled by real TeX into an SVG fragment, from docs/readme_figure.py.

VecTeX is a library-level reimplementation of the rendering and normalization boundary behind TexText. It needs neither Inkscape nor access to the destination document.

Install

VecTeX is on PyPI and needs Python 3.12 or newer:

python -m pip install vectex

Rendering also needs, on PATH, a TeX engine (pdflatex, xelatex, or lualatex) with the standalone class and the preview, amsmath, and varwidth packages, and dvisvgm; TeX Live and MiKTeX ship all of them. dvisvgm reads PDF through MuPDF's mutool or a Ghostscript older than 10.01, so with a current Ghostscript install mutool too. On Debian and Ubuntu:

sudo apt install texlive-latex-extra dvisvgm mupdf-tools
sudo apt install texlive-xetex texlive-luatex   # only for xelatex and lualatex

The optional object-model adapters install with an extra:

python -m pip install 'vectex[svg-py]'   # import svg
python -m pip install 'vectex[drawsvg]'  # import drawsvg
python -m pip install 'vectex[all]'

Quick start

import vectex

fragment = vectex.render(r"mass $m$ and energy $E = mc^2$")
label = vectex.render(r"$E = mc^2$", size_pt=8, id_prefix="einstein")
vector = vectex.render(r"$\bm{n}$", extra_packages=("bm",))
purple = vectex.render(r"$\mu$", color="#7a1fa2")  # glyphs and rules alike

svg_text = fragment.to_svg()  # the <g> fragment as a string
lxml_group = fragment.to_lxml()  # a fresh lxml element
document = fragment.to_svg_document()  # a complete, openable SVG document
fragment.write_svg_document("label.svg")  # the same document, on disk

print(fragment.width, fragment.height, fragment.view_box)
print(fragment.source, fragment.engine, fragment.metadata)

The outer group of label has id="einstein-root", and rendering the same input again produces the same bytes.

TeX input is always a literal document body, the convention TexText uses: $...$ marks inline mathematics, \[...\] display mathematics, and everything else is prose. Complete environments such as align* work directly. The default preamble loads amsmath, so \text{...} works in math. A \\ in prose, such as r"qubit\\frequency $f_{01}$", breaks a label into centered rows that keep the first row's baseline. Adjacent displays such as r"\[E_0=-\Delta/2\]\[E_1=\Delta/2\]" form one cropped stack of centered rows; use render_many() for separate fragments.

The default preamble is the standalone class with amsmath, and extra_packages=("bm",) adds \usepackage lines to it. A nonempty preamble replaces the whole preamble instead: it must contain \documentclass, and must load amsmath itself if the source needs it. The two options are mutually exclusive. With standalone, every fragment is cropped tightly to what TeX typesets, so a border= class option has no effect; any other class, such as preamble=r"\documentclass{article}", gives full pages and no baseline.

size_pt=7 scales a fragment so that the class's nominal size (10 pt unless a class option such as 11pt says otherwise) comes out at 7 pt; scale=0.7 scales it directly, and passing both is an error. Both are geometric: TeX still typesets at the class size, and TikZ lengths shrink with the text.

Every call uses a fresh temporary directory and runs two stages:

source -> pdflatex/xelatex/lualatex -> PDF -> dvisvgm -> SVG -> lxml -> <g>

Command line

The vectex command renders a document body to a fragment on standard output:

vectex '$E = mc^2$'
vectex '$E = mc^2$' --as-doc > einstein.svg      # a complete SVG document
vectex '$E = mc^2$' --as-doc -o einstein.svg     # or write it with -o/--output
vectex --input equation.tex --as-doc -o equation.svg
printf '%s\n' '$E = mc^2$' | vectex - --as-doc > einstein.svg

-i/--input PATH reads the source from a UTF-8 file. --extra-package NAME (repeatable) loads a package; --preamble or --preamble-file preamble.tex supplies a complete preamble, and the file form also records its absolute path for later TexText editing. --cache-dir PATH reuses render records, --refresh recompiles the selected one, and --no-cache skips the cache. --size-pt or --scale, --color, --id-prefix, --engine, --timeout, and --executable NAME=PATH (repeatable) mirror the Python options. The converter, compiler and converter arguments, baseline, unique_ids, textext_compatible, textext_alignment, and batches have no command-line form. vectex --help lists everything; vectex --version reports the installed version. An invalid option exits with status 2 and usage text; a TeX error, a converter failure, or a missing tool exits with status 1.

TikZ pictures

A tikzpicture is an ordinary document body:

arrow = vectex.render(
    r"\begin{tikzpicture}\draw[->] (0,0) -- (2,1);\end{tikzpicture}",
    extra_packages=["tikz"],
)

extra_packages emits only \usepackage lines, so \usetikzlibrary, \pgfplotsset, and package options need a complete preamble. examples/bloch_sphere.py is a worked example, and examples/figure_labels.py renders a batch of labels in one compilation. The rendering guide records one converter limitation: dvisvgm does not translate PDF shadings, so \shade and ball color can yield a correctly sized fragment with nothing painted in it. VecTeX emits EmptyRenderWarning for SVGs without graphical elements outside definitions, including cached results. This detects wholly empty output, not partially missing shading. Flat fills, patterns, opacity, and pgfplots convert normally. Use warnings.simplefilter("error", vectex.EmptyRenderWarning) to treat the warning as a build failure.

Embedding and adapters

to_lxml() returns a fresh element on every call, so editing it cannot mutate the fragment's canonical serialization:

from lxml import etree

document = etree.fromstring('<svg xmlns="http://www.w3.org/2000/svg"/>')
document.append(fragment.to_lxml())

The optional adapters wrap the complete normalized XML rather than translating arbitrary SVG into a smaller object model:

import drawsvg
import svg

svg_py_document = svg.SVG(
    width=fragment.width_px,
    height=fragment.height_px,
    elements=[fragment.to_svg_py()],
)

drawing = drawsvg.Drawing(fragment.width_px, fragment.height_px)
drawing.append(fragment.to_drawsvg())

Both documents measure in CSS px, so the wrappers scale the label from TeX points to px and it keeps its size; width_px, height_px, and baseline_px measure it there, and unit="pt" gives the canonical group unscaled. Either wrapper can also place the label by a point of its box, its baseline included: fragment.to_svg_py(x=40, y=120, valign="baseline").

TexText editing in Inkscape

TexText recognizes editable nodes by attributes in its namespace on the outer <g>. VecTeX writes the current compatibility fields: encoded source, compiler, converter marker, preamble-file path, scale, alignment, version, and transform Jacobian. Insert the outer group itself and select that whole group before opening TexText; TexText rejects a selected nested path or subgroup. Because both tools treat the stored text as a document body, it recompiles without translation.

TexText stores its preamble as a file path, while VecTeX accepts preamble content. If re-editing must use the same custom preamble, pass both:

fragment = vectex.render(
    r"$\operatorname{rank}(A)$",
    preamble="\\documentclass{standalone}\n\\usepackage{amsmath}",
    textext_preamble_file="/absolute/shared/preamble.tex",
)

The path must be accessible to TexText on the editing machine; the preamble content itself is kept in VecTeX metadata. Pass textext_compatible=False to omit all TexText attributes.

Executable discovery and configuration

Built-in components resolve pdflatex, xelatex, lualatex, and dvisvgm with shutil.which. Exact overrides make discovery explicit:

fragment = vectex.render(
    "$x+y$",
    executable_overrides={
        "pdflatex": "/opt/texlive/bin/pdflatex",
        "dvisvgm": "/opt/texlive/bin/dvisvgm",
    },
    timeout=20,
    compiler_args=("--synctex=0",),
    converter_args=("--precision=6",),
)

Argument options are sequences, never shell strings, and VecTeX never uses shell=True. timeout limits each external process separately, TeX and then dvisvgm, and enters neither the cache key nor the ids. Nonzero exits and timeouts raise CompilationError or ConversionError carrying argv, return code, stdout, and stderr. A TeX failure's message is the TeX error and its context rather than the whole log, and in a batch it names the source that failed, as in TeX error in source 2 of 3, '$\\foo$': Undefined control sequence.; the full log stays in stdout. A missing executable raises MissingExecutableError, which is not a CompilationError; every VecTeX error derives from VectexError, and the rendering guide lists them. Applications may implement the Compiler and Converter protocols and pass component objects instead of built-in names.

Batch rendering and disk cache

render_many([a, b, ...]) shares one compiler and one dvisvgm invocation while preserving each expression's crop and baseline. A source may also be a RenderItem carrying any option that shapes its fragment; options left as None take the batch value (merge rules). Items that can share a compilation are grouped, so labels differing only in size still cost one invocation, while an item with its own preamble or engine forms its own group. Fragments come back in input order. render() also accepts a RenderItem.

The persistent cache is enabled with cache_dir= or the VECTEX_CACHE_DIR environment variable; once the variable is set, every call and the vectex command use the cache, unless a call passes cache_dir=False. Entries are keyed by the render options, executable overrides included, and by each tool's identity: for a built-in component, the executable's resolved path and the first line of its --version output. A TeX or dvisvgm upgrade therefore misses old records, but an update to a LaTeX package, class, or font, to mutool, or to Ghostscript does not; pass refresh=True or call vectex.clear_cache(directory) after one. Entries are checksummed and written atomically, and corrupt ones are treated as misses. A cache that cannot be written costs only its records: the render goes on and warns with CacheWarning.

refresh=True recompiles every item of the call and is ignored without a cache. clear_cache() removes only VecTeX's records and returns how many it removed. A custom component is cached only if it has an identity() method (custom components); with a cache enabled, by either route, a component without one and unique_ids=True raise ConfigurationError. Enabling caching or refreshing a record does not change SVG IDs or serialization.

Fragment guarantees

A successful render returns exactly one SVG <g> root with:

  • copied converter definitions, in order of first use, and visible elements;
  • a deterministic, input-derived id prefix, with href, xlink:href, and url(#...) references rewritten, including inside inline styles;
  • the source viewport represented by an inner matrix transform;
  • normalized width, height, view box, scale, and measurable baseline;
  • TeX's default black, in fills and strokes, as currentColor, so a label takes the CSS color of where it is placed -- or color= at render time -- rules included, while parts given another colour in TeX keep it (an explicit black cannot be told from the default and follows too);
  • deterministic repeated serialization;
  • a VecTeX <metadata> child with format version, source, engine, converter, geometry, and preamble/options;
  • TexText edit attributes unless disabled.

Identical render inputs serialize identically. Without id_prefix, the id prefix is hashed from the inputs that shape the output: the source, preamble or extra packages, engine and converter names and arguments, scale or size_pt, baseline, color, and the TexText options. The timeout, executable overrides, cache settings, and installed tools do not enter it. id_prefix="einstein" names the outer group einstein-root and rewritten definitions einstein-0, …; the CLI spells it --id-prefix einstein. render_many(..., id_prefix="labels") suffixes the prefix by input position. To embed the same render more than once in one SVG, give each copy its own id_prefix. unique_ids=True draws a random prefix instead, so its output differs on every run, and it cannot be combined with id_prefix or a disk cache.

Security and trust assumptions

The XML parser disables DTD loading, entity resolution, network access, recovery, comments, and processing instructions. Normalization rejects scripts, foreignObject, SVG animation, event handlers, <style> elements, class attributes, CSS imports, escapes, and comments, xml:base, external hrefs and URLs, duplicate ids, and unresolved local references, so a fragment carries no active content and no dependency on destination stylesheets beyond the color it inherits.

LaTeX is a powerful program, not a sandbox. VecTeX passes -no-shell-escape to the built-in engines, but a malicious source or compiler option can still read files or consume resources. Compile only trusted source, and use an OS or container sandbox for untrusted input. Executable overrides, preamble content, and extra argv values are trusted configuration.

Stability and supported platforms

VecTeX is beta. The API is settled enough to build on, and what may still change is written down here rather than discovered in a release.

Public API. The supported surface is the names in vectex.__all__ and the vectex command-line interface. Module layout, private helpers, and the shape of intermediate records may change in any release. The package ships a py.typed marker, so consumers type-check against that surface.

Versioning. VecTeX follows Semantic Versioning. Before 1.0 a minor release may still break the public API, but never silently: every break is listed under Changed or Removed in CHANGELOG.md. Patch releases never break it. 1.0 will freeze the surface above for the 1.x series.

Output stability is a separate and stronger promise: identical render inputs serialize identically (see Fragment guarantees). With the same VecTeX version and the same TeX and dvisvgm installation, treat a change in bytes for unchanged inputs as a bug.

Supported platforms. VecTeX is pure Python driving an external TeX toolchain, so support is bounded by what continuous integration exercises:

Python Unit tests Real TeX toolchain
Linux 3.12 and 3.14 yes yes (Python 3.14): pdflatex, xelatex, lualatex, dvisvgm
macOS 3.14 yes not exercised
Windows 3.14 yes not exercised

Python 3.13 is not built separately, on the assumption that a pure-Python package passing at both ends of its range passes between them. On macOS and Windows the unit suite covers executable lookup and path handling, but no job installs TeX: rendering is expected to work with TeX on PATH and is not verified by CI. Report a platform failure as a bug; the gap is in the testing, not the intent.

Development

Unit tests use checked-in SVG fixtures and mocked subprocesses, and need no TeX installation:

uv sync --all-extras
uv run ruff format --check .
uv run ruff check .
uv run ty check
uv run pytest
uv run python -m build

The real-tool tests are opt-in locally because they need TeX. They include tests/test_examples.py, which runs every script in examples/:

VECTEX_RUN_INTEGRATION=1 uv run pytest -m integration
uv run python examples/bloch_sphere.py --theta 50 --phi 40

Continuous integration runs the real-tool tests against TeX Live on every push, every release, and nightly. That job first checks that each executable is present, because the tests skip themselves when a tool is missing and a silent skip would look like a pass.

VecTeX is developed alongside FigWorks, which composes multi-panel figures, and two other producers of editable SVG: VecView (layered 3D schematics) and VecWire (circuit schematics). All four share one premise: figures generated from code, with stable ids and byte-identical output, that stay editable in Inkscape.

VecTeX does not depend on any of them. A composition tool needs only VectexFragment.to_svg_document(), so the integration costs no import in either direction, and VecTeX works the same against any destination that accepts SVG.

Documentation

  • Rendering: the pipeline, baselines, batches and cache, custom components, TexText contract, trust policy, and errors
  • Fragments: placement, anchors, ids, colour, metadata, and caller integration
  • Development: toolchain and releases

Build the site locally with uv run zensical build.

License

MIT

Metadata

Release files for vectex 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vectex 0.5.0
File Size Uploaded Provenance Actions
vectex-0.5.0.tar.gz 89.4 kB With provenance Details for vectex-0.5.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for vectex 0.5.0
File Interpreter ABI Platform Actions
vectex-0.5.0-py3-none-any.whl Python 3 none any Details for vectex-0.5.0-py3-none-any.whl

Total release size: 134.0 kB

Release files / vectex-0.5.0.tar.gz

Download URL vectex-0.5.0.tar.gz
Size 89.4 kB
Tags Source
SHA-256 checksum
How to use checksums
768c318bf324b82125013a58997b309585b7b0033dd32f9e8ac9fd1fa6ba1907
BLAKE2b-256 checksum
How to use checksums
79cbfca98faf813be7188271d7cfc31127684174ab0b7e2e3880a2924ee51fca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.

Transparency log

Release files / vectex-0.5.0-py3-none-any.whl

Download URL vectex-0.5.0-py3-none-any.whl
Size 44.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a47bed2bbfc53821d3711e0f66f480a6a9ca51e347a367cb08218d6f8f71c8d8
BLAKE2b-256 checksum
How to use checksums
926474a13040bcbd56efc5ffe341b03ae4858a93bd0b3d74a337153fcaf1fc23
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.

Transparency log

Security for vectex 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for vectex-0.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page