mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Construct TexTemplate class to convert tex to svg
This commit is contained in:
parent
edca4a93fa
commit
f0984487ea
10 changed files with 177 additions and 228 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
|
@ -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}
|
|
@ -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]")
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue