Source code for manim.utils.tex_file_writing

"""Interface for writing, compiling, and converting ``.tex`` files.

.. SEEALSO::

    :mod:`.mobject.svg.tex_mobject`

"""

from __future__ import annotations

import hashlib
import re
import subprocess
import unicodedata
from collections.abc import Generator, Iterable, Sequence
from pathlib import Path
from re import Match
from typing import Any

from manim.utils.tex import TexTemplate

from .. import config, logger

__all__ = ["tex_to_svg_file"]


[docs] def tex_hash(expression: Any) -> str: id_str = str(expression) hasher = hashlib.sha256() hasher.update(id_str.encode()) # Truncating at 16 bytes for cleanliness return hasher.hexdigest()[:16]
[docs] def tex_to_svg_file( expression: str, environment: str | None = None, tex_template: TexTemplate | None = None, ) -> Path: r"""Takes a tex expression and returns the svg version of the compiled tex Parameters ---------- expression String containing the TeX expression to be rendered, e.g. ``\\sqrt{2}`` or ``foo`` environment The string containing the environment in which the expression should be typeset, e.g. ``align*`` tex_template Template class used to typesetting. If not set, use default template set via `config["tex_template"]` Returns ------- :class:`Path` Path to generated SVG file. """ if tex_template is None: tex_template = config["tex_template"] tex_file = generate_tex_file(expression, environment, tex_template) # check if svg already exists svg_file = tex_file.with_suffix(".svg") if svg_file.exists(): return svg_file 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) if not config["no_latex_cleanup"]: delete_nonsvg_files() return svg_file
[docs] def generate_tex_file( expression: str, environment: str | None = None, tex_template: TexTemplate | None = None, ) -> Path: r"""Takes a tex expression (and an optional tex environment), and returns a fully formed tex file ready for compilation. Parameters ---------- expression String containing the TeX expression to be rendered, e.g. ``\\sqrt{2}`` or ``foo`` environment The string containing the environment in which the expression should be typeset, e.g. ``align*`` tex_template Template class used to typesetting. If not set, use default template set via `config["tex_template"]` Returns ------- :class:`Path` Path to generated TeX file """ if tex_template is None: tex_template = config["tex_template"] if environment is not None: output = tex_template.get_texcode_for_expression_in_env(expression, environment) else: output = tex_template.get_texcode_for_expression(expression) tex_dir = config.get_dir("tex_dir") tex_dir.mkdir(parents=True, exist_ok=True) result = tex_dir / (tex_hash(output) + ".tex") if not result.exists(): logger.info( "Writing %(expression)s to %(path)s", {"expression": expression, "path": f"{result}"}, ) result.write_text(output, encoding="utf-8") return result
[docs] def make_tex_compilation_command( tex_compiler: str, output_format: str, tex_file: Path, tex_dir: Path ) -> list[str]: """Prepares the TeX compilation command, i.e. the TeX compiler name and all necessary CLI flags. Parameters ---------- tex_compiler String containing the compiler to be used, e.g. ``pdflatex`` or ``lualatex`` output_format String containing the output format generated by the compiler, e.g. ``.dvi`` or ``.pdf`` tex_file File name of TeX file to be typeset. tex_dir Path to the directory where compiler output will be stored. Returns ------- :class:`list[str]` Compilation command according to given parameters """ if tex_compiler in {"latex", "pdflatex", "luatex", "lualatex"}: command = [ tex_compiler, "-interaction=batchmode", f"-output-format={output_format[1:]}", "-halt-on-error", f"-output-directory={tex_dir.as_posix()}", f"{tex_file.as_posix()}", ] elif tex_compiler == "xelatex": if output_format == ".xdv": outflag = ["-no-pdf"] elif output_format == ".pdf": outflag = [] else: raise ValueError("xelatex output is either pdf or xdv") command = [ "xelatex", *outflag, "-interaction=batchmode", "-halt-on-error", f"-output-directory={tex_dir.as_posix()}", f"{tex_file.as_posix()}", ] else: raise ValueError(f"Tex compiler {tex_compiler} unknown.") return command
[docs] def insight_inputenc_error(matching: Match[str]) -> Generator[str]: code_point = chr(int(matching[1], 16)) name = unicodedata.name(code_point) yield f"TexTemplate does not support character '{name}' (U+{matching[1]})." yield "See the documentation for manim.mobject.svg.tex_mobject for details on using a custom TexTemplate."
[docs] def insight_package_not_found_error(matching: Match[str]) -> Generator[str]: yield f"You do not have package {matching[1]} installed." yield f"Install {matching[1]} it using your LaTeX package manager, or check for typos."
[docs] def compile_tex(tex_file: Path, tex_compiler: str, output_format: str) -> Path: """Compiles a tex_file into a .dvi or a .xdv or a .pdf Parameters ---------- tex_file File name of TeX file to be typeset. tex_compiler String containing the compiler to be used, e.g. ``pdflatex`` or ``lualatex`` output_format String containing the output format generated by the compiler, e.g. ``.dvi`` or ``.pdf`` Returns ------- :class:`Path` Path to generated output file in desired format (DVI, XDV or PDF). """ result = tex_file.with_suffix(output_format) tex_dir = config.get_dir("tex_dir") if not result.exists(): command = make_tex_compilation_command( tex_compiler, output_format, tex_file, tex_dir, ) cp = subprocess.run(command, stdout=subprocess.DEVNULL) if cp.returncode != 0: log_file = tex_file.with_suffix(".log") print_all_tex_errors(log_file, tex_compiler, tex_file) raise ValueError( f"{tex_compiler} error converting to" f" {output_format[1:]}. See log output above or" f" the log file: {log_file}", ) return result
[docs] def convert_to_svg(dvi_file: Path, extension: str, page: int = 1) -> Path: """Converts a .dvi, .xdv, or .pdf file into an svg using dvisvgm. Parameters ---------- dvi_file File name of the input file to be converted. extension String containing the file extension and thus indicating the file type, e.g. ``.dvi`` or ``.pdf`` page Page to be converted if input file is multi-page. Returns ------- :class:`Path` Path to generated SVG file. """ result = dvi_file.with_suffix(".svg") if not result.exists(): command = [ "dvisvgm", *(["--pdf"] if extension == ".pdf" else []), f"--page={page}", "--no-fonts", "--verbosity=0", f"--output={result.as_posix()}", f"{dvi_file.as_posix()}", ] subprocess.run(command, stdout=subprocess.DEVNULL) # if the file does not exist now, this means conversion failed if not result.exists(): raise ValueError( f"Your installation does not support converting {dvi_file.suffix} files to SVG." f" Consider updating dvisvgm to at least version 2.4." f" If this does not solve the problem, please refer to our troubleshooting guide at:" f" https://docs.manim.community/en/stable/faq/general.html#my-installation-" f"does-not-support-converting-pdf-to-svg-help", ) return result
[docs] def delete_nonsvg_files(additional_endings: Iterable[str] = ()) -> None: """Deletes every file that does not have a suffix in ``(".svg", ".tex", *additional_endings)`` Parameters ---------- additional_endings Additional endings to whitelist """ tex_dir = config.get_dir("tex_dir") file_suffix_whitelist = {".svg", ".tex", *additional_endings} for f in tex_dir.iterdir(): if f.suffix not in file_suffix_whitelist: f.unlink()
LATEX_ERROR_INSIGHTS = [ ( r"inputenc Error: Unicode character (?:.*) \(U\+([0-9a-fA-F]+)\)", insight_inputenc_error, ), ( r"LaTeX Error: File `(.*?[clsty])' not found", insight_package_not_found_error, ), ]