diff --git a/manimlib/constants.py b/manimlib/constants.py index f0bc3269..2be127a0 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -64,6 +64,56 @@ JOINT_TYPE_MAP = { "miter": 3, } +# Related to Tex +PRESET_PREAMBLE = { + "default": ( + "\\usepackage[english]{babel}", + "\\usepackage[utf8]{inputenc}", + "\\usepackage[T1]{fontenc}", + "\\usepackage{amsmath}", + "\\usepackage{amssymb}", + "\\usepackage{dsfont}", + "\\usepackage{setspace}", + "\\usepackage{tipa}", + "\\usepackage{relsize}", + "\\usepackage{textcomp}", + "\\usepackage{mathrsfs}", + "\\usepackage{calligra}", + "\\usepackage{wasysym}", + "\\usepackage{ragged2e}", + "\\usepackage{physics}", + "\\usepackage{xcolor}", + "\\usepackage{microtype}", + "\\usepackage{pifont}", + "\\DisableLigatures{encoding = *, family = * }", + "\\linespread{1}", + ), + "ctex": ( + "\\usepackage[UTF8]{ctex}", + "\\usepackage[english]{babel}", + "\\usepackage{amsmath}", + "\\usepackage{amssymb}", + "\\usepackage{dsfont}", + "\\usepackage{setspace}", + "\\usepackage{tipa}", + "\\usepackage{relsize}", + "\\usepackage{textcomp}", + "\\usepackage{mathrsfs}", + "\\usepackage{calligra}", + "\\usepackage{wasysym}", + "\\usepackage{ragged2e}", + "\\usepackage{physics}", + "\\usepackage{xcolor}", + "\\usepackage{microtype}", + "\\linespread{1}", + ), + "minimized": ( + "\\usepackage{amsmath}", + "\\usepackage{amssymb}", + "\\usepackage{xcolor}", + ), +} + # Related to Text NORMAL = "NORMAL" ITALIC = "ITALIC" diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index c948a8e5..d5e89605 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -18,13 +18,12 @@ directories: temporary_storage: "" tex: executable: "latex" - template_file: "tex_template.tex" - intermediate_filetype: "dvi" - text_to_replace: "[tex_expression]" + intermediate_filetype: ".dvi" + preamble: "default" # For ctex, use the following configuration # executable: "xelatex -no-pdf" - # template_file: "ctex_template.tex" - # intermediate_filetype: "xdv" + # intermediate_filetype: ".xdv" + # preamble: "ctex" universal_import_line: "from manimlib import *" style: font: "Consolas" diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 149f313f..52947ec9 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -1,9 +1,7 @@ from __future__ import annotations from manimlib.mobject.svg.string_mobject import StringMobject -from manimlib.utils.tex_file_writing import display_during_execution -from manimlib.utils.tex_file_writing import get_tex_config -from manimlib.utils.tex_file_writing import tex_to_svg_file +from manimlib.utils.tex_file_writing import TexTemplate from typing import TYPE_CHECKING @@ -37,6 +35,7 @@ class MTex(StringMobject): "alignment": "\\centering", "tex_environment": "align*", "tex_to_color_map": {}, + "tex_template": None, } def __init__(self, tex_string: str, **kwargs): @@ -60,17 +59,13 @@ class MTex(StringMobject): self.tex_string, self.alignment, self.tex_environment, - self.tex_to_color_map + self.tex_to_color_map, + self.tex_template ) def get_file_path_by_content(self, content: str) -> str: - tex_config = get_tex_config() - full_tex = tex_config["tex_body"].replace( - tex_config["text_to_replace"], - content - ) - with display_during_execution(f"Writing \"{self.string}\""): - file_path = tex_to_svg_file(full_tex) + tex_template = self.tex_template or TexTemplate() + file_path = tex_template.get_svg_file_path(content) return file_path # Parsing diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index fb444608..2ed272c6 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -12,9 +12,7 @@ from manimlib.mobject.geometry import Line from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.config_ops import digest_config -from manimlib.utils.tex_file_writing import display_during_execution -from manimlib.utils.tex_file_writing import get_tex_config -from manimlib.utils.tex_file_writing import tex_to_svg_file +from manimlib.utils.tex_file_writing import TexTemplate from typing import TYPE_CHECKING @@ -44,6 +42,7 @@ class SingleStringTex(SVGMobject): "alignment": "\\centering", "math_mode": True, "organize_left_to_right": False, + "tex_template": None, } def __init__(self, tex_string: str, **kwargs): @@ -64,27 +63,21 @@ class SingleStringTex(SVGMobject): self.path_string_config, self.tex_string, self.alignment, - self.math_mode + self.math_mode, + self.tex_template ) def get_file_path(self) -> str: - full_tex = self.get_tex_file_body(self.tex_string) - with display_during_execution(f"Writing \"{self.tex_string}\""): - file_path = tex_to_svg_file(full_tex) + content = self.get_tex_file_body(self.tex_string) + tex_template = self.tex_template or TexTemplate() + file_path = tex_template.get_svg_file_path(content) return file_path def get_tex_file_body(self, tex_string: str) -> str: new_tex = self.get_modified_expression(tex_string) if self.math_mode: new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" - - new_tex = self.alignment + "\n" + new_tex - - tex_config = get_tex_config() - return tex_config["tex_body"].replace( - tex_config["text_to_replace"], - new_tex - ) + return self.alignment + "\n" + new_tex def get_modified_expression(self, tex_string: str) -> str: return self.modify_special_strings(tex_string.strip()) diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 93623c31..3e45753c 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -18,7 +18,7 @@ from manimlib.utils.config_ops import digest_config from manimlib.utils.customization import get_customization from manimlib.utils.directories import get_downloads_dir from manimlib.utils.directories import get_text_dir -from manimlib.utils.tex_file_writing import tex_hash +from manimlib.utils.simple_functions import hash_string from typing import TYPE_CHECKING @@ -180,7 +180,7 @@ class MarkupText(StringMobject): self.line_width )) svg_file = os.path.join( - get_text_dir(), tex_hash(hash_content) + ".svg" + get_text_dir(), hash_string(hash_content) + ".svg" ) if not os.path.exists(svg_file): self.markup_to_svg(content, svg_file) diff --git a/manimlib/tex_templates/ctex_template.tex b/manimlib/tex_templates/ctex_template.tex deleted file mode 100644 index 65ff5df1..00000000 --- a/manimlib/tex_templates/ctex_template.tex +++ /dev/null @@ -1,25 +0,0 @@ -\documentclass[preview]{standalone} -\usepackage[UTF8]{ctex} - -\usepackage[english]{babel} -\usepackage{amsmath} -\usepackage{amssymb} -\usepackage{dsfont} -\usepackage{setspace} -\usepackage{tipa} -\usepackage{relsize} -\usepackage{textcomp} -\usepackage{mathrsfs} -\usepackage{calligra} -\usepackage{wasysym} -\usepackage{ragged2e} -\usepackage{physics} -\usepackage{xcolor} -\usepackage{microtype} -\linespread{1} - -\begin{document} - -[tex_expression] - -\end{document} diff --git a/manimlib/tex_templates/tex_template.tex b/manimlib/tex_templates/tex_template.tex deleted file mode 100644 index 5f665b61..00000000 --- a/manimlib/tex_templates/tex_template.tex +++ /dev/null @@ -1,28 +0,0 @@ -\documentclass[preview]{standalone} - -\usepackage[english]{babel} -\usepackage[utf8]{inputenc} -\usepackage[T1]{fontenc} -\usepackage{amsmath} -\usepackage{amssymb} -\usepackage{dsfont} -\usepackage{setspace} -\usepackage{tipa} -\usepackage{relsize} -\usepackage{textcomp} -\usepackage{mathrsfs} -\usepackage{calligra} -\usepackage{wasysym} -\usepackage{ragged2e} -\usepackage{physics} -\usepackage{xcolor} -\usepackage{microtype} -\usepackage{pifont} -\DisableLigatures{encoding = *, family = * } -\linespread{1} - -\begin{document} - -[tex_expression] - -\end{document} diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 2e5d4b32..04381ae5 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -9,7 +9,7 @@ from rich import box from rich.console import Console from rich.prompt import Confirm from rich.prompt import Prompt -from rich.rule import Rule +from rich.rule import Rule from rich.table import Table from typing import TYPE_CHECKING @@ -44,9 +44,8 @@ def init_customization() -> None: }, "tex": { "executable": "", - "template_file": "", "intermediate_filetype": "", - "text_to_replace": "[tex_expression]", + "preamble": "", }, "universal_import_line": "from manimlib import *", "style": { @@ -83,7 +82,7 @@ def init_customization() -> None: # print("Initialize configuration") try: scope = Prompt.ask( - " Select the scope of the configuration", + " Select the scope of the configuration", choices=["global", "local"], default="local" ) @@ -129,13 +128,13 @@ def init_customization() -> None: ) if tex == "latex": tex_config["executable"] = "latex" - tex_config["template_file"] = "tex_template.tex" - tex_config["intermediate_filetype"] = "dvi" + tex_config["intermediate_filetype"] = ".dvi" + tex_config["preamble"] = "default" else: tex_config["executable"] = "xelatex -no-pdf" - tex_config["template_file"] = "ctex_template.tex" - tex_config["intermediate_filetype"] = "xdv" - + tex_config["intermediate_filetype"] = ".xdv" + tex_config["preamble"] = "ctex" + console.print("[bold]Styles:[/bold]") configuration["style"]["background_color"] = Prompt.ask( " Which [bold]background color[/bold] do you want [italic](hex code)", @@ -172,7 +171,7 @@ def init_customization() -> None: file_name = os.path.join(os.getcwd(), "custom_config.yml") with open(file_name, "w", encoding="utf-8") as f: yaml.dump(configuration, f) - + console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!") console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]") diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index 1371a744..143bf350 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -1,4 +1,5 @@ from functools import lru_cache +import hashlib import inspect import math @@ -76,3 +77,9 @@ def binary_search(function, else: return None return mh + + +def hash_string(string): + # Truncating at 16 bytes for cleanliness + hasher = hashlib.sha256(string.encode()) + return hasher.hexdigest()[:16] diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 557d08be..d3e1792a 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -1,143 +1,102 @@ from __future__ import annotations -from contextlib import contextmanager -import hashlib import os -import sys +import re from manimlib.config import get_custom_config -from manimlib.config import get_manim_dir +from manimlib.constants import PRESET_PREAMBLE from manimlib.logger import log from manimlib.utils.directories import get_tex_dir - - -SAVED_TEX_CONFIG = {} - - -def get_tex_config() -> dict[str, str]: - """ - Returns a dict which should look something like this: - { - "executable": "latex", - "template_file": "tex_template.tex", - "intermediate_filetype": "dvi", - "text_to_replace": "YourTextHere", - "tex_body": "..." - } - """ - # Only load once, then save thereafter - if not SAVED_TEX_CONFIG: - custom_config = get_custom_config() - SAVED_TEX_CONFIG.update(custom_config["tex"]) - # Read in template file - template_filename = os.path.join( - get_manim_dir(), "manimlib", "tex_templates", - SAVED_TEX_CONFIG["template_file"], - ) - with open(template_filename, "r", encoding="utf-8") as file: - SAVED_TEX_CONFIG["tex_body"] = file.read() - return SAVED_TEX_CONFIG - - -def tex_hash(tex_file_content: str) -> int: - # Truncating at 16 bytes for cleanliness - hasher = hashlib.sha256(tex_file_content.encode()) - return hasher.hexdigest()[:16] - - -def tex_to_svg_file(tex_file_content: str) -> str: - svg_file = os.path.join( - get_tex_dir(), tex_hash(tex_file_content) + ".svg" - ) - if not os.path.exists(svg_file): - # If svg doesn't exist, create it - tex_to_svg(tex_file_content, svg_file) - return svg_file - - -def tex_to_svg(tex_file_content: str, svg_file: str) -> str: - tex_file = svg_file.replace(".svg", ".tex") - with open(tex_file, "w", encoding="utf-8") as outfile: - outfile.write(tex_file_content) - svg_file = dvi_to_svg(tex_to_dvi(tex_file)) - - # Cleanup superfluous documents - tex_dir, name = os.path.split(svg_file) - stem, end = name.split(".") - for file in filter(lambda s: s.startswith(stem), os.listdir(tex_dir)): - if not file.endswith(end): - os.remove(os.path.join(tex_dir, file)) - - return svg_file - - -def tex_to_dvi(tex_file: str) -> str: - tex_config = get_tex_config() - program = tex_config["executable"] - file_type = tex_config["intermediate_filetype"] - result = tex_file.replace(".tex", "." + file_type) - if not os.path.exists(result): - commands = [ - program, - "-interaction=batchmode", - "-halt-on-error", - f"-output-directory=\"{os.path.dirname(tex_file)}\"", - f"\"{tex_file}\"", - ">", - os.devnull - ] - exit_code = os.system(" ".join(commands)) - if exit_code != 0: - log_file = tex_file.replace(".tex", ".log") - log.error("LaTeX Error! Not a worry, it happens to the best of us.") - with open(log_file, "r", encoding="utf-8") as file: - for line in file.readlines(): - if line.startswith("!"): - log.debug(f"The error could be: `{line[2:-1]}`") - raise LatexError() - return result - - -def dvi_to_svg(dvi_file: str) -> str: - """ - Converts a dvi, which potentially has multiple slides, into a - directory full of enumerated pngs corresponding with these slides. - Returns a list of PIL Image objects for these images sorted as they - where in the dvi - """ - file_type = get_tex_config()["intermediate_filetype"] - result = dvi_file.replace("." + file_type, ".svg") - if not os.path.exists(result): - commands = [ - "dvisvgm", - "\"{}\"".format(dvi_file), - "-n", - "-v", - "0", - "-o", - "\"{}\"".format(result), - ">", - os.devnull - ] - os.system(" ".join(commands)) - return result - - -# TODO, perhaps this should live elsewhere -@contextmanager -def display_during_execution(message: str) -> None: - # Only show top line - to_print = message.split("\n")[0] - 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") - +from manimlib.utils.simple_functions import hash_string class LatexError(Exception): pass + + +class TexTemplate: + def __init__(self, preamble_type: str | None = None): + tex_config = get_custom_config()["tex"] + self.executable = tex_config["executable"] + self.dvi_ext = tex_config["intermediate_filetype"] + if preamble_type is None: + preamble_type = tex_config["preamble"] + self.preamble = list(PRESET_PREAMBLE.get( + preamble_type, PRESET_PREAMBLE["default"] + )) + + def __hash__(self) -> int: + return hash(self.get_tex_file_content("")) + + def get_tex_file_content(self, content: str) -> str: + return "\n\n".join(( + "\\documentclass[preview]{standalone}", + "\n".join(self.preamble), + "\\begin{document}", + content, + "\\end{document}" + )) + "\n" + + def get_svg_file_path(self, content: str) -> str: + full_tex = self.get_tex_file_content(content) + hash_code = hash_string(full_tex) + tex_dir = get_tex_dir() + root = os.path.join(tex_dir, hash_code) + svg_file_path = root + ".svg" + if os.path.exists(svg_file_path): + return svg_file_path + + # If svg doesn't exist, create it + replaced_content = content.replace("\n", " ") + displayed_msg = f"Writing \"{replaced_content}\"" + max_characters = os.get_terminal_size().columns - 1 + if len(displayed_msg) > max_characters: + displayed_msg = displayed_msg[:max_characters - 3] + "..." + print(displayed_msg, end="\r") + + with open(root + ".tex", "w", encoding="utf-8") as tex_file: + tex_file.write(full_tex) + + # tex to dvi + if os.system(" ".join(( + self.executable, + "-interaction=batchmode", + "-halt-on-error", + f"-output-directory=\"{tex_dir}\"", + 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}{self.dvi_ext}\"", + "-n", + "-v", + "0", + "-o", + f"\"{svg_file_path}\"", + ">", + os.devnull + ))) + + # Cleanup superfluous documents + for ext in (".tex", self.dvi_ext, ".log", ".aux"): + try: + os.remove(root + ext) + except FileNotFoundError: + pass + + print(" " * len(displayed_msg), end="\r") + return svg_file_path + + def add_preamble(self, *preamble_strs: str): + self.preamble.extend(preamble_strs) + return self