tex_file_writing.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. from __future__ import annotations
  2. from contextlib import contextmanager
  3. import os
  4. import re
  5. import yaml
  6. from manimlib.config import get_custom_config
  7. from manimlib.config import get_manim_dir
  8. from manimlib.logger import log
  9. from manimlib.utils.directories import get_tex_dir
  10. from manimlib.utils.simple_functions import hash_string
  11. SAVED_TEX_CONFIG = {}
  12. def get_tex_template_config(template_name: str) -> dict[str, str]:
  13. name = template_name.replace(" ", "_").lower()
  14. with open(os.path.join(
  15. get_manim_dir(), "manimlib", "tex_templates.yml"
  16. ), encoding="utf-8") as tex_templates_file:
  17. templates_dict = yaml.safe_load(tex_templates_file)
  18. if name not in templates_dict:
  19. log.warning(
  20. "Cannot recognize template '%s', falling back to 'default'.",
  21. name
  22. )
  23. name = "default"
  24. return templates_dict[name]
  25. def get_tex_config() -> dict[str, str]:
  26. """
  27. Returns a dict which should look something like this:
  28. {
  29. "template": "default",
  30. "compiler": "latex",
  31. "preamble": "..."
  32. }
  33. """
  34. # Only load once, then save thereafter
  35. if not SAVED_TEX_CONFIG:
  36. template_name = get_custom_config()["style"]["tex_template"]
  37. template_config = get_tex_template_config(template_name)
  38. SAVED_TEX_CONFIG.update({
  39. "template": template_name,
  40. "compiler": template_config["compiler"],
  41. "preamble": template_config["preamble"]
  42. })
  43. return SAVED_TEX_CONFIG
  44. def tex_content_to_svg_file(
  45. content: str, template: str, additional_preamble: str,
  46. short_tex: str
  47. ) -> str:
  48. tex_config = get_tex_config()
  49. if not template or template == tex_config["template"]:
  50. compiler = tex_config["compiler"]
  51. preamble = tex_config["preamble"]
  52. else:
  53. config = get_tex_template_config(template)
  54. compiler = config["compiler"]
  55. preamble = config["preamble"]
  56. if additional_preamble:
  57. preamble += "\n" + additional_preamble
  58. full_tex = "\n\n".join((
  59. "\\documentclass[preview]{standalone}",
  60. preamble,
  61. "\\begin{document}",
  62. content,
  63. "\\end{document}"
  64. )) + "\n"
  65. svg_file = os.path.join(
  66. get_tex_dir(), hash_string(full_tex) + ".svg"
  67. )
  68. if not os.path.exists(svg_file):
  69. # If svg doesn't exist, create it
  70. with display_during_execution("Writing " + short_tex):
  71. create_tex_svg(full_tex, svg_file, compiler)
  72. return svg_file
  73. def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
  74. if compiler == "latex":
  75. program = "latex"
  76. dvi_ext = ".dvi"
  77. elif compiler == "xelatex":
  78. program = "xelatex -no-pdf"
  79. dvi_ext = ".xdv"
  80. else:
  81. raise NotImplementedError(
  82. f"Compiler '{compiler}' is not implemented"
  83. )
  84. # Write tex file
  85. root, _ = os.path.splitext(svg_file)
  86. with open(root + ".tex", "w", encoding="utf-8") as tex_file:
  87. tex_file.write(full_tex)
  88. # tex to dvi
  89. if os.system(" ".join((
  90. program,
  91. "-interaction=batchmode",
  92. "-halt-on-error",
  93. f"-output-directory=\"{os.path.dirname(svg_file)}\"",
  94. f"\"{root}.tex\"",
  95. ">",
  96. os.devnull
  97. ))):
  98. log.error(
  99. "LaTeX Error! Not a worry, it happens to the best of us."
  100. )
  101. error_str = ""
  102. with open(root + ".log", "r", encoding="utf-8") as log_file:
  103. error_match_obj = re.search(r"(?<=\n! ).*\n.*\n", log_file.read())
  104. if error_match_obj:
  105. error_str = error_match_obj.group()
  106. log.debug(
  107. f"The error could be:\n`{error_str}`",
  108. )
  109. raise LatexError(error_str)
  110. # dvi to svg
  111. os.system(" ".join((
  112. "dvisvgm",
  113. f"\"{root}{dvi_ext}\"",
  114. "-n",
  115. "-v",
  116. "0",
  117. "-o",
  118. f"\"{svg_file}\"",
  119. ">",
  120. os.devnull
  121. )))
  122. # Cleanup superfluous documents
  123. for ext in (".tex", dvi_ext, ".log", ".aux"):
  124. try:
  125. os.remove(root + ext)
  126. except FileNotFoundError:
  127. pass
  128. # TODO, perhaps this should live elsewhere
  129. @contextmanager
  130. def display_during_execution(message: str):
  131. # Merge into a single line
  132. to_print = message.replace("\n", " ")
  133. max_characters = os.get_terminal_size().columns - 1
  134. if len(to_print) > max_characters:
  135. to_print = to_print[:max_characters - 3] + "..."
  136. try:
  137. print(to_print, end="\r")
  138. yield
  139. finally:
  140. print(" " * len(to_print), end="\r")
  141. class LatexError(Exception):
  142. pass