VecTeX
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.
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, andurl(#...)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 CSScolorof where it is placed -- orcolor=at render time -- rules included, while parts given another colour in TeX keep it (an explicitblackcannot 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.
Related projects
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.