Clean up tex_file_writing

This commit is contained in:
Grant Sanderson 2024-12-04 20:30:53 -06:00
parent 129e512b0c
commit ac01b144e8
3 changed files with 82 additions and 76 deletions

View file

@ -7,7 +7,7 @@ import re
from manimlib.constants import BLACK, WHITE from manimlib.constants import BLACK, WHITE
from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.tex_file_writing import tex_to_svg from manimlib.utils.tex_file_writing import latex_to_svg
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -79,7 +79,7 @@ class SingleStringTex(SVGMobject):
def get_svg_string_by_content(self, content: str) -> str: def get_svg_string_by_content(self, content: str) -> str:
return get_cached_value( return get_cached_value(
key=hash_string(str((content, self.template, self.additional_preamble))), key=hash_string(str((content, self.template, self.additional_preamble))),
value_func=lambda: tex_to_svg(content, self.template, self.additional_preamble), value_func=lambda: latex_to_svg(content, self.template, self.additional_preamble),
message=f"Writing {self.tex_string}..." message=f"Writing {self.tex_string}..."
) )

View file

@ -9,7 +9,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.cache import get_cached_value from manimlib.utils.cache import get_cached_value
from manimlib.utils.color import color_to_hex from manimlib.utils.color import color_to_hex
from manimlib.utils.color import hex_to_int from manimlib.utils.color import hex_to_int
from manimlib.utils.tex_file_writing import tex_to_svg from manimlib.utils.tex_file_writing import latex_to_svg
from manimlib.utils.tex import num_tex_symbols from manimlib.utils.tex import num_tex_symbols
from manimlib.utils.simple_functions import hash_string from manimlib.utils.simple_functions import hash_string
from manimlib.logger import log from manimlib.logger import log
@ -88,7 +88,7 @@ class Tex(StringMobject):
def get_svg_string_by_content(self, content: str) -> str: def get_svg_string_by_content(self, content: str) -> str:
return get_cached_value( return get_cached_value(
key=hash_string(str((content, self.template, self.additional_preamble))), key=hash_string(str((content, self.template, self.additional_preamble))),
value_func=lambda: tex_to_svg(content, self.template, self.additional_preamble), value_func=lambda: latex_to_svg(content, self.template, self.additional_preamble),
message=f"Writing {self.tex_string}..." message=f"Writing {self.tex_string}..."
) )

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import os import os
import re import re
import yaml import yaml
import subprocess
from pathlib import Path from pathlib import Path
import tempfile import tempfile
@ -19,9 +20,8 @@ SAVED_TEX_CONFIG = {}
def get_tex_template_config(template_name: str) -> dict[str, str]: def get_tex_template_config(template_name: str) -> dict[str, str]:
name = template_name.replace(" ", "_").lower() name = template_name.replace(" ", "_").lower()
with open(os.path.join( template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml")
get_manim_dir(), "manimlib", "tex_templates.yml" with open(template_path, encoding="utf-8") as tex_templates_file:
), encoding="utf-8") as tex_templates_file:
templates_dict = yaml.safe_load(tex_templates_file) templates_dict = yaml.safe_load(tex_templates_file)
if name not in templates_dict: if name not in templates_dict:
log.warning( log.warning(
@ -53,23 +53,8 @@ def get_tex_config() -> dict[str, str]:
return SAVED_TEX_CONFIG return SAVED_TEX_CONFIG
def tex_to_svg( def get_full_tex(content: str, preamble: str = ""):
content: str, return "\n\n".join((
template: str,
additional_preamble: str,
) -> str:
tex_config = get_tex_config()
if not template or template == tex_config["template"]:
compiler = tex_config["compiler"]
preamble = tex_config["preamble"]
else:
config = get_tex_template_config(template)
compiler = config["compiler"]
preamble = config["preamble"]
if additional_preamble:
preamble += "\n" + additional_preamble
full_tex = "\n\n".join((
"\\documentclass[preview]{standalone}", "\\documentclass[preview]{standalone}",
preamble, preamble,
"\\begin{document}", "\\begin{document}",
@ -77,14 +62,32 @@ def tex_to_svg(
"\\end{document}" "\\end{document}"
)) + "\n" )) + "\n"
with tempfile.NamedTemporaryFile(suffix='.svg', mode='r+') as tmp:
create_tex_svg(full_tex, tmp.name, compiler)
# Read the contents
tmp.seek(0)
return tmp.read()
def latex_to_svg(
latex: str,
template: str = "",
additional_preamble: str = ""
) -> str:
"""Convert LaTeX string to SVG string.
Args:
latex: LaTeX source code
template: Path to a template LaTeX file
additional_preamble: String including any added "\\usepackage{...}" style imports
Returns:
str: SVG source code
Raises:
LatexError: If LaTeX compilation fails
NotImplementedError: If compiler is not supported
"""
tex_config = get_tex_config()
if template and template != tex_config["template"]:
tex_config = get_tex_template_config(template)
compiler = tex_config["compiler"]
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
if compiler == "latex": if compiler == "latex":
program = "latex" program = "latex"
dvi_ext = ".dvi" dvi_ext = ".dvi"
@ -92,57 +95,60 @@ def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
program = "xelatex -no-pdf" program = "xelatex -no-pdf"
dvi_ext = ".xdv" dvi_ext = ".xdv"
else: else:
raise NotImplementedError( raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
f"Compiler '{compiler}' is not implemented"
) preamble = tex_config["preamble"] + "\n" + additional_preamble
full_tex = get_full_tex(latex, preamble)
# Write intermediate files to a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
base_path = os.path.join(temp_dir, "working")
tex_path = base_path + ".tex"
dvi_path = base_path + dvi_ext
# Write tex file # Write tex file
root, _ = os.path.splitext(svg_file) with open(tex_path, "w", encoding="utf-8") as tex_file:
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
tex_file.write(full_tex) tex_file.write(full_tex)
# tex to dvi # Run latex compiler
if os.system(" ".join(( process = subprocess.run(
program, [
program.split()[0], # Split for xelatex case
"-interaction=batchmode", "-interaction=batchmode",
"-halt-on-error", "-halt-on-error",
f"-output-directory=\"{os.path.dirname(svg_file)}\"", "-output-directory=" + temp_dir,
f"\"{root}.tex\"", tex_path
">", ] + (["--no-pdf"] if compiler == "xelatex" else []),
os.devnull capture_output=True,
))): text=True
log.error(
"LaTeX Error! Not a worry, it happens to the best of us."
) )
if process.returncode != 0:
# Handle error
error_str = "" error_str = ""
with open(root + ".log", "r", encoding="utf-8") as log_file: log_path = base_path + ".log"
error_match_obj = re.search(r"(?<=\n! ).*\n.*\n", log_file.read()) if os.path.exists(log_path):
if error_match_obj: with open(log_path, "r", encoding="utf-8") as log_file:
error_str = error_match_obj.group() content = log_file.read()
log.debug( error_match = re.search(r"(?<=\n! ).*\n.*\n", content)
f"The error could be:\n`{error_str}`", if error_match:
) error_str = error_match.group()
raise LatexError(error_str) raise LatexError(error_str or "LaTeX compilation failed")
# dvi to svg # Run dvisvgm and capture output directly
os.system(" ".join(( process = subprocess.run(
[
"dvisvgm", "dvisvgm",
f"\"{root}{dvi_ext}\"", dvi_path,
"-n", "-n", # no fonts
"-v", "-v", "0", # quiet
"0", "--stdout", # output to stdout instead of file
"-o", ],
f"\"{svg_file}\"", capture_output=True
">", )
os.devnull
)))
# Cleanup superfluous documents # Return SVG string
for ext in (".tex", dvi_ext, ".log", ".aux"): return process.stdout.decode('utf-8')
try:
os.remove(root + ext)
except FileNotFoundError:
pass
class LatexError(Exception): class LatexError(Exception):