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,
}
# 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"

View file

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

View file

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

View file

@ -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())

View file

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

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

@ -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]")

View file

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

View file

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