3b1b-manim/manimlib/utils/tex_file_writing.py
Grant Sanderson e0e7e24351 Move display_during_execturion call
Such that it only gets called when a new svg needs to be written
2022-12-19 17:01:06 -08:00

164 lines
4.6 KiB
Python

from __future__ import annotations
from contextlib import contextmanager
import os
import re
import yaml
from manimlib.config import get_custom_config
from manimlib.config import get_manim_dir
from manimlib.logger import log
from manimlib.utils.directories import get_tex_dir
from manimlib.utils.simple_functions import hash_string
SAVED_TEX_CONFIG = {}
def get_tex_template_config(template_name: str) -> dict[str, str]:
name = template_name.replace(" ", "_").lower()
with open(os.path.join(
get_manim_dir(), "manimlib", "tex_templates.yml"
), encoding="utf-8") as tex_templates_file:
templates_dict = yaml.safe_load(tex_templates_file)
if name not in templates_dict:
log.warning(
"Cannot recognize template '%s', falling back to 'default'.",
name
)
name = "default"
return templates_dict[name]
def get_tex_config() -> dict[str, str]:
"""
Returns a dict which should look something like this:
{
"template": "default",
"compiler": "latex",
"preamble": "..."
}
"""
# Only load once, then save thereafter
if not SAVED_TEX_CONFIG:
template_name = get_custom_config()["style"]["tex_template"]
template_config = get_tex_template_config(template_name)
SAVED_TEX_CONFIG.update({
"template": template_name,
"compiler": template_config["compiler"],
"preamble": template_config["preamble"]
})
return SAVED_TEX_CONFIG
def tex_content_to_svg_file(
content: str, template: str, additional_preamble: str,
short_tex: 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}",
preamble,
"\\begin{document}",
content,
"\\end{document}"
)) + "\n"
svg_file = os.path.join(
get_tex_dir(), hash_string(full_tex) + ".svg"
)
if not os.path.exists(svg_file):
# If svg doesn't exist, create it
with display_during_execution("Writing " + short_tex):
create_tex_svg(full_tex, svg_file, compiler)
return svg_file
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
if compiler == "latex":
program = "latex"
dvi_ext = ".dvi"
elif compiler == "xelatex":
program = "xelatex -no-pdf"
dvi_ext = ".xdv"
else:
raise NotImplementedError(
f"Compiler '{compiler}' is not implemented"
)
# Write tex file
root, _ = os.path.splitext(svg_file)
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
tex_file.write(full_tex)
# tex to dvi
if os.system(" ".join((
program,
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
f"\"{root}.tex\"",
">",
os.devnull
))):
log.error(
"LaTeX Error! Not a worry, it happens to the best of us."
)
error_str = ""
with open(root + ".log", "r", encoding="utf-8") as log_file:
error_match_obj = re.search(r"(?<=\n! ).*\n.*\n", log_file.read())
if error_match_obj:
error_str = error_match_obj.group()
log.debug(
f"The error could be:\n`{error_str}`",
)
raise LatexError(error_str)
# dvi to svg
os.system(" ".join((
"dvisvgm",
f"\"{root}{dvi_ext}\"",
"-n",
"-v",
"0",
"-o",
f"\"{svg_file}\"",
">",
os.devnull
)))
# Cleanup superfluous documents
for ext in (".tex", dvi_ext, ".log", ".aux"):
try:
os.remove(root + ext)
except FileNotFoundError:
pass
# TODO, perhaps this should live elsewhere
@contextmanager
def display_during_execution(message: str):
# Merge into a single line
to_print = message.replace("\n", " ")
max_characters = os.get_terminal_size().columns - 1
if len(to_print) > max_characters:
to_print = to_print[:max_characters - 3] + "..."
try:
print(to_print, end="\r")
yield
finally:
print(" " * len(to_print), end="\r")
class LatexError(Exception):
pass