Construct TexTemplate class to convert tex to svg

This commit is contained in:
YishiMichael 2022-05-21 15:56:03 +08:00
parent edca4a93fa
commit f0984487ea
No known key found for this signature in database
GPG key ID: EC615C0C5A86BC80
10 changed files with 177 additions and 228 deletions

View file

@ -64,6 +64,56 @@ JOINT_TYPE_MAP = {
"miter": 3, "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 # Related to Text
NORMAL = "NORMAL" NORMAL = "NORMAL"
ITALIC = "ITALIC" ITALIC = "ITALIC"

View file

@ -18,13 +18,12 @@ directories:
temporary_storage: "" temporary_storage: ""
tex: tex:
executable: "latex" executable: "latex"
template_file: "tex_template.tex" intermediate_filetype: ".dvi"
intermediate_filetype: "dvi" preamble: "default"
text_to_replace: "[tex_expression]"
# For ctex, use the following configuration # For ctex, use the following configuration
# executable: "xelatex -no-pdf" # executable: "xelatex -no-pdf"
# template_file: "ctex_template.tex" # intermediate_filetype: ".xdv"
# intermediate_filetype: "xdv" # preamble: "ctex"
universal_import_line: "from manimlib import *" universal_import_line: "from manimlib import *"
style: style:
font: "Consolas" font: "Consolas"

View file

@ -1,9 +1,7 @@
from __future__ import annotations from __future__ import annotations
from manimlib.mobject.svg.string_mobject import StringMobject 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 TexTemplate
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -37,6 +35,7 @@ class MTex(StringMobject):
"alignment": "\\centering", "alignment": "\\centering",
"tex_environment": "align*", "tex_environment": "align*",
"tex_to_color_map": {}, "tex_to_color_map": {},
"tex_template": None,
} }
def __init__(self, tex_string: str, **kwargs): def __init__(self, tex_string: str, **kwargs):
@ -60,17 +59,13 @@ class MTex(StringMobject):
self.tex_string, self.tex_string,
self.alignment, self.alignment,
self.tex_environment, 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: def get_file_path_by_content(self, content: str) -> str:
tex_config = get_tex_config() tex_template = self.tex_template or TexTemplate()
full_tex = tex_config["tex_body"].replace( file_path = tex_template.get_svg_file_path(content)
tex_config["text_to_replace"],
content
)
with display_during_execution(f"Writing \"{self.string}\""):
file_path = tex_to_svg_file(full_tex)
return file_path return file_path
# Parsing # Parsing

View file

@ -12,9 +12,7 @@ from manimlib.mobject.geometry import Line
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.config_ops import digest_config 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 TexTemplate
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -44,6 +42,7 @@ class SingleStringTex(SVGMobject):
"alignment": "\\centering", "alignment": "\\centering",
"math_mode": True, "math_mode": True,
"organize_left_to_right": False, "organize_left_to_right": False,
"tex_template": None,
} }
def __init__(self, tex_string: str, **kwargs): def __init__(self, tex_string: str, **kwargs):
@ -64,27 +63,21 @@ class SingleStringTex(SVGMobject):
self.path_string_config, self.path_string_config,
self.tex_string, self.tex_string,
self.alignment, self.alignment,
self.math_mode self.math_mode,
self.tex_template
) )
def get_file_path(self) -> str: def get_file_path(self) -> str:
full_tex = self.get_tex_file_body(self.tex_string) content = self.get_tex_file_body(self.tex_string)
with display_during_execution(f"Writing \"{self.tex_string}\""): tex_template = self.tex_template or TexTemplate()
file_path = tex_to_svg_file(full_tex) file_path = tex_template.get_svg_file_path(content)
return file_path return file_path
def get_tex_file_body(self, tex_string: str) -> str: def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string) new_tex = self.get_modified_expression(tex_string)
if self.math_mode: if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
return self.alignment + "\n" + new_tex
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
)
def get_modified_expression(self, tex_string: str) -> str: def get_modified_expression(self, tex_string: str) -> str:
return self.modify_special_strings(tex_string.strip()) return self.modify_special_strings(tex_string.strip())

View file

@ -18,7 +18,7 @@ from manimlib.utils.config_ops import digest_config
from manimlib.utils.customization import get_customization from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_downloads_dir from manimlib.utils.directories import get_downloads_dir
from manimlib.utils.directories import get_text_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 from typing import TYPE_CHECKING
@ -180,7 +180,7 @@ class MarkupText(StringMobject):
self.line_width self.line_width
)) ))
svg_file = os.path.join( 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): if not os.path.exists(svg_file):
self.markup_to_svg(content, svg_file) self.markup_to_svg(content, svg_file)

View file

@ -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}

View file

@ -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}

View file

@ -44,9 +44,8 @@ def init_customization() -> None:
}, },
"tex": { "tex": {
"executable": "", "executable": "",
"template_file": "",
"intermediate_filetype": "", "intermediate_filetype": "",
"text_to_replace": "[tex_expression]", "preamble": "",
}, },
"universal_import_line": "from manimlib import *", "universal_import_line": "from manimlib import *",
"style": { "style": {
@ -129,12 +128,12 @@ def init_customization() -> None:
) )
if tex == "latex": if tex == "latex":
tex_config["executable"] = "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: else:
tex_config["executable"] = "xelatex -no-pdf" 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]") console.print("[bold]Styles:[/bold]")
configuration["style"]["background_color"] = Prompt.ask( configuration["style"]["background_color"] = Prompt.ask(

View file

@ -1,4 +1,5 @@
from functools import lru_cache from functools import lru_cache
import hashlib
import inspect import inspect
import math import math
@ -76,3 +77,9 @@ def binary_search(function,
else: else:
return None return None
return mh return mh
def hash_string(string):
# Truncating at 16 bytes for cleanliness
hasher = hashlib.sha256(string.encode())
return hasher.hexdigest()[:16]

View file

@ -1,143 +1,102 @@
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager
import hashlib
import os import os
import sys import re
from manimlib.config import get_custom_config 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.logger import log
from manimlib.utils.directories import get_tex_dir from manimlib.utils.directories import get_tex_dir
from manimlib.utils.simple_functions import hash_string
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")
class LatexError(Exception): class LatexError(Exception):
pass 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