Skip to content

Manim's text pipeline: LaTeX → dvi/xdv → SVG → VMobject

How Manim Community turns a string into animatable geometry. There is no glyph rasterizer in Manim: every piece of text — a LaTeX formula, a Pango-shaped label, a Typst snippet — is compiled to an SVG of vector paths and then imported into the same cubic-VMobject scene graph as any hand-drawn shape. This is the typesetting companion to the Manim Community deep-dive; the object model it feeds is scene-graph.md, shared terms are in ../concepts.md.


How it works

Three independent front-ends produce SVG; one importer converts SVG to VMobject outlines.

text
MathTex / Tex ──► tex_to_svg_file ──► latex/xelatex (subprocess) ──► .dvi/.xdv/.pdf ──► dvisvgm --no-fonts ──┐
Text / MarkupText ──► manimpango.text2svg (Pango + HarfBuzz shaping) ─────────────────────────────► .svg ───┼─► SVGMobject
TypstMobject ──► typst_to_svg_file ──► typst.compile(format="svg") (in-process) ───────────────────► .svg ──┘   (svgelements → cubic points)

LaTeX: a multi-step subprocess pipeline

tex_file_writing.py is described in its module docstring as the "Interface for writing, compiling, and converting .tex files" (tex_file_writing.py:1). tex_to_svg_file (tex_file_writing.py:35) drives four stages:

python
# tex_file_writing.py:58 — write .tex, compile to dvi/xdv/pdf, convert to svg
tex_file = generate_tex_file(expression, environment, tex_template)
dvi_file = compile_tex(tex_file, tex_template.tex_compiler, tex_template.output_format)
svg_file = convert_to_svg(dvi_file, tex_template.output_format)
  1. generate_tex_file (tex_file_writing.py:76) wraps the expression in the template and writes it under tex_dir, named by tex_hash — a truncated SHA-256: hasher.hexdigest()[:16] (tex_file_writing.py:32). This is the on-disk cache key for compiled TeX.
  2. compile_tex (tex_file_writing.py:181) shells out with subprocess.run(command, stdout=subprocess.DEVNULL) (:214). The compiler is configurable: make_tex_compilation_command (tex_file_writing.py:118) builds latex/pdflatex/ lualatex invocations with -output-format=, -halt-on-error, and -output-directory=, and special-cases xelatex, which uses -no-pdf to emit .xdv instead of PDF (tex_file_writing.py:149). A list of compilers can be chained.
  3. convert_to_svg (tex_file_writing.py:226) runs dvisvgm:
python
# tex_file_writing.py:245 — dvisvgm turns glyphs into vector PATHS, not fonts
command = [
    "dvisvgm",
    *(["--pdf"] if extension == ".pdf" else []),
    f"--page={page}",
    "--no-fonts",          # ← convert font glyphs to <path> outlines
    "--verbosity=0",
    f"--output={result.as_posix()}",
    f"{dvi_file.as_posix()}",
]

The --no-fonts flag is the crux: it makes dvisvgm emit each glyph as a standalone vector path rather than referencing an embedded font, so the SVG Manim imports is pure geometry. If conversion fails the error suggests "updating dvisvgm to at least version 2.4" (tex_file_writing.py:261).

The mobject side: SingleStringMathTex(SVGMobject) (tex_mobject.py:46) calls tex_to_svg_file in its constructor (tex_mobject.py:81) with tex_environment="align*" by default, and passes path_string_config={"should_subdivide_sharp_curves": True, "should_remove_null_curves": True} (tex_mobject.py:92) to the SVG importer. MathTex (tex_mobject.py:227) and Tex (tex_mobject.py:607) subclass it to split a formula into addressable sub-mobjects.

Non-math text: manimpango (Pango + HarfBuzz)

Text and MarkupText (both SVGMobject, text_mobject.py:302 and :841) route through manimpango (import manimpango, text_mobject.py:66) — Manim's Cython binding over Pango, which does Unicode text shaping (HarfBuzz) and font selection. Text._text2svg (text_mobject.py:799) calls:

python
# text_mobject.py:818 — Pango shapes and rasterizes text into an SVG of paths
svg_file = manimpango.text2svg(
    settings, size, line_spacing, self.disable_ligatures,
    str(file_name.resolve()), START_X, START_Y, width, height, self.text)

MarkupText uses MarkupUtils.text2svg(...) (text_mobject.py:1367) for Pango-markup input, and register_font (text_mobject.py:1487, manimpango.register_font) makes a font file visible to manimpango.list_fonts(). The output SVG is again pure paths, consumed by the same importer.

Typst: in-process, single-step

The newest front-end skips the subprocess dance. typst_file_writing.py"Interface for writing, compiling, and converting .typ files via the typst Python package" (typst_file_writing.py:1) — compiles Typst markup directly to SVG in-process:

python
# typst_file_writing.py:100 — one call, no dvisvgm intermediate
svg_bytes = typst_compiler.compile(str(typ_file), format="svg", font_paths=font_paths or [])

It caches by the same scheme (_typst_hash = hexdigest()[:16], typst_file_writing.py:32) and is gated behind the optional typst>=0.14 extra (pyproject.toml); importing TypstMobject without it raises "TypstMobject requires the 'typst' Python package" (typst_file_writing.py:73).


Glyph outline extraction: SVG → cubic VMobject

All three front-ends converge on SVGMobject (svg_mobject.py), which parses the file with svgelements (import svgelements as se, svg_mobject.py:11): se.SVG.parse(...) (svg_mobject.py:205), then walks groups and shapes (get_mobjects_from, svg_mobject.py:264) turning each se.Path into a VMobjectFromSVGPath (svg_mobject.py:385). The per-segment conversion is handle_commands (svg_mobject.py:561) — this is the glyph-outline-extraction step, and it is basis-aware:

python
# svg_mobject.py:575 — the cubic (Cairo) branch: elevate everything to 4-point cubics
if self.n_points_per_curve == 4:
    def add_quad(start, cp, end):        # quadratic → cubic (exact 2/3 elevation)
        add_cubic(start, (start + cp + cp) / 3, (cp + cp + end) / 3, end)
    def add_line(start, end):            # line → cubic (handles on the segment)
        add_cubic(start, (start + start + end) / 3, (start + end + end) / 3, end)

For each SVG path segment — se.Move, se.Line, se.QuadraticBezier, se.CubicBezier, se.Close (svg_mobject.py:622-649) — the cubic backend stores cubics verbatim ([start, cp1, cp2, end], svg_mobject.py:582) and elevates lines and quadratics into cubics, the same 2/3 rule the hand-built geometry uses (scene-graph.md, bezier-eval.d). The OpenGL branch instead lowers SVG cubics to two quadratics via get_quadratic_approximation_of_cubic (svg_mobject.py:602). The result is written straight into the mobject's points array (svg_mobject.py:651) — from there, text is indistinguishable from any other VMobject and animates identically.

NOTE

Because text is geometry, a glyph is not pixel-snapped or hinted the way a font rasterizer would. The path outlines are scaled to font_size after import (SingleStringMathTex.font_size, tex_mobject.py:112), so text remains resolution-independent and interpolates point-for-point in a Transform once point counts are aligned (scene-graph.md).


Typesetting & text (analysis)

Mapping the front-ends onto the survey's axis:

ConcernLaTeX (MathTex/Tex)Pango (Text/MarkupText)Typst (TypstMobject)
Enginelatex/xelatex/lualatex subprocessmanimpango (Pango + HarfBuzz)typst package, in-process
Intermediate.dvi / .xdv / .pdfnone (direct to SVG)none (direct to SVG)
SVG converterdvisvgm --no-fontsPango's Cairo SVG surfaceTypst's SVG exporter
ShapingTeX's math + font metricsHarfBuzz complex-script shapingTypst's own shaper
Cache keysha256(tex)[:16]_text2hash (color-aware)sha256(source)[:16]
Dependencyfull TeX install + dvisvgm (external)manimpango (bundled binding)optional typst extra

The through-line is that all text lands in the cubic VMobject layout on the default Cairo backend, so latex-to-svg and text-shaping are front-ends to the one vector-geometry model; nothing about text has its own rasterization or its own animation code. The determinism of the SHA-256 filenames also feeds the render cache described in caching.md.


Sources

Related: the Manim Community deep-dive · scene-graph.md · caching.md · ../concepts.md · manimgl. Probe: bezier-eval.d.