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_font_preamble(tex_font: str) -> str: name = re.sub(r"[^a-zA-Z]", "_", tex_font).lower() with open(os.path.join( get_manim_dir(), "manimlib", "tex_fonts.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 font '%s', falling back to 'default'.", name ) name = "default" result = templates_dict[name] if name not in ("default", "ctex", "basic", "ctex_basic", "blank"): result = templates_dict["basic"] + "\n" + result return result def get_tex_config() -> dict[str, str]: """ Returns a dict which should look something like this: { "compiler": "latex", "font": "default", "preamble": "..." } """ # Only load once, then save thereafter if not SAVED_TEX_CONFIG: style_config = get_custom_config()["style"] SAVED_TEX_CONFIG.update({ "compiler": style_config["tex_compiler"], "font": style_config["tex_font"], "preamble": get_tex_font_preamble(style_config["tex_font"]) }) return SAVED_TEX_CONFIG def tex_content_to_svg_file( content: str, tex_font: str, additional_preamble: str ) -> str: tex_config = get_tex_config() if not tex_font or tex_font == tex_config["font"]: preamble = tex_config["preamble"] else: preamble = get_tex_font_preamble(tex_font) 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 create_tex_svg(full_tex, svg_file, tex_config["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." ) with open(root + ".log", "r", encoding="utf-8") as log_file: error_match_obj = re.search(r"(?<=\n! ).*", log_file.read()) if error_match_obj: log.debug( "The error could be: `%s`", error_match_obj.group() ) raise LatexError() # 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) -> None: # 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