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.
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:
# 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)generate_tex_file(tex_file_writing.py:76) wraps the expression in the template and writes it undertex_dir, named bytex_hash— a truncated SHA-256:hasher.hexdigest()[:16](tex_file_writing.py:32). This is the on-disk cache key for compiled TeX.compile_tex(tex_file_writing.py:181) shells out withsubprocess.run(command, stdout=subprocess.DEVNULL)(:214). The compiler is configurable:make_tex_compilation_command(tex_file_writing.py:118) buildslatex/pdflatex/lualatexinvocations with-output-format=,-halt-on-error, and-output-directory=, and special-casesxelatex, which uses-no-pdfto emit.xdvinstead of PDF (tex_file_writing.py:149). A list of compilers can be chained.convert_to_svg(tex_file_writing.py:226) runsdvisvgm:
# 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:
# 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:
# 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:
# 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:
| Concern | LaTeX (MathTex/Tex) | Pango (Text/MarkupText) | Typst (TypstMobject) |
|---|---|---|---|
| Engine | latex/xelatex/lualatex subprocess | manimpango (Pango + HarfBuzz) | typst package, in-process |
| Intermediate | .dvi / .xdv / .pdf | none (direct to SVG) | none (direct to SVG) |
| SVG converter | dvisvgm --no-fonts | Pango's Cairo SVG surface | Typst's SVG exporter |
| Shaping | TeX's math + font metrics | HarfBuzz complex-script shaping | Typst's own shaper |
| Cache key | sha256(tex)[:16] | _text2hash (color-aware) | sha256(source)[:16] |
| Dependency | full 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
manim/utils/tex_file_writing.py—tex_to_svg_file,compile_tex(subprocess),make_tex_compilation_command,convert_to_svg(dvisvgm --no-fonts),tex_hash.manim/mobject/text/tex_mobject.py—SingleStringMathTex,MathTex,Tex; SVG import config.manim/mobject/text/text_mobject.py—Text,MarkupText,manimpango.text2svg/MarkupUtils.text2svg,register_font.manim/utils/typst_file_writing.py— in-process Typst → SVG.manim/mobject/svg/svg_mobject.py—svgelementsparse,handle_commandsbasis-aware SVG-path → cubicpoints.
Related: the Manim Community deep-dive · scene-graph.md · caching.md · ../concepts.md · manimgl. Probe: bezier-eval.d.