diff --git a/example_scenes/customtex.py b/example_scenes/customtex.py deleted file mode 100644 index 3825aea33c..0000000000 --- a/example_scenes/customtex.py +++ /dev/null @@ -1,17 +0,0 @@ -from manim import * - -class ExampleFileScene(Scene): - def construct(self): - text=TexMobject(r"\vv{vb}") - #text=TextMobject(r"$\vv{vb}$") - self.play(Write(text)) - -class ExampleScene(Scene): - def construct(self): - tpl=TexTemplate() - tpl.append_package(["esvect",["f"]]) - config.register_tex_template(tpl) - - #text=TextMobject(r"$\vv{vb}$") - text=TexMobject(r"\vv{vb}") - self.play(Write(text)) diff --git a/manim/__init__.py b/manim/__init__.py index eb741d49c6..48403e23ed 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -68,4 +68,3 @@ from .utils.sounds import * from .utils.space_ops import * from .utils.strings import * -from .utils.tex import * diff --git a/manim/__main__.py b/manim/__main__.py index 1eaeefde5f..08dc1d325b 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -7,7 +7,6 @@ def main(): args = config.parse_cli() cfg = config.get_configuration(args) config.initialize_directories(cfg) - config.initialize_tex(cfg) extract_scene.main(cfg) diff --git a/manim/config.py b/manim/config.py index 02e240bee3..a353a42184 100644 --- a/manim/config.py +++ b/manim/config.py @@ -3,14 +3,12 @@ import os import sys import types -import manim.constants as consts -from .utils.tex import * from . import constants from . import dirs from .logger import logger -__all__ = ["parse_cli", "get_configuration", "initialize_directories","register_tex_template","initialize_tex"] +__all__ = ["parse_cli", "get_configuration", "initialize_directories"] def parse_cli(): @@ -142,12 +140,7 @@ def parse_cli(): "--text_dir", help="Directory to write text", ) - parser.add_argument( - "--tex_template", - help="Specify a custom TeX template file", - ) return parser.parse_args() - except argparse.ArgumentError as err: logger.error(str(err)) sys.exit(2) @@ -183,7 +176,6 @@ def get_configuration(args): "video_dir": args.video_dir, "tex_dir": args.tex_dir, "text_dir": args.text_dir, - "tex_template": args.tex_template, } # Camera configuration @@ -289,39 +281,3 @@ def initialize_directories(config): dirs.VIDEO_DIR = dir_config["video_dir"] dirs.TEX_DIR = dir_config["tex_dir"] dirs.TEXT_DIR = dir_config["text_dir"] - -def register_tex_template(tpl): - """Register the given LaTeX template for later use. - - Parameters - ---------- - tpl : :class:`~.TexTemplate` - The LaTeX template to register. - """ - consts.TEX_TEMPLATE = tpl - -def initialize_tex(config): - """Safely create a LaTeX template object from a file. - If file is not readable, the default template file is used. - - Parameters - ---------- - filename : :class:`str` - The name of the file with the LaTeX template. - """ - filename="" - if config["tex_template"]: - filename = os.path.expanduser(config["tex_template"]) - if filename and not os.access(filename, os.R_OK): - # custom template not available, fallback to default - logger.warning( - f"Custom TeX template {filename} not found or not readable. " - "Falling back to the default template." - ) - filename = "" - if filename: - # still having a filename -> use the file - consts.TEX_TEMPLATE = TexTemplateFromFile(filename=filename) - else: - # use the default template - consts.TEX_TEMPLATE = TexTemplate() diff --git a/manim/constants.py b/manim/constants.py index 3486168957..c1b2e8687b 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -2,58 +2,6 @@ import os from .logger import logger -MEDIA_DIR = "" -VIDEO_DIR = "" -VIDEO_OUTPUT_DIR = "" -TEX_DIR = "" -TEXT_DIR = "" -TEX_TEMPLATE = None - -def initialize_directories(config): - global MEDIA_DIR - global VIDEO_DIR - global VIDEO_OUTPUT_DIR - global TEX_DIR - global TEXT_DIR - - video_path_specified = config["video_dir"] or config["video_output_dir"] - - if not (video_path_specified and config["tex_dir"]): - if config["media_dir"]: - MEDIA_DIR = config["media_dir"] - else: - MEDIA_DIR = os.path.join( - os.path.expanduser('~'), - "Dropbox (3Blue1Brown)/3Blue1Brown Team Folder" - ) - if not os.path.isdir(MEDIA_DIR): - MEDIA_DIR = "./media" - print( - f"Media will be written to {MEDIA_DIR + os.sep}. You can change " - "this behavior with the --media_dir flag." - ) - else: - if config["media_dir"]: - print( - "Ignoring --media_dir, since both --tex_dir and a video " - "directory were both passed" - ) - - TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex") - TEXT_DIR = os.path.join(MEDIA_DIR, "texts") - if not video_path_specified: - VIDEO_DIR = os.path.join(MEDIA_DIR, "videos") - VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos") - elif config["video_output_dir"]: - VIDEO_OUTPUT_DIR = config["video_output_dir"] - else: - VIDEO_DIR = config["video_dir"] - - for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR, TEXT_DIR]: - if folder != "" and not os.path.exists(folder): - os.makedirs(folder) - - NOT_SETTING_FONT_MSG=''' You haven't set font. If you are not using English, this may cause text rendering problem. @@ -72,23 +20,19 @@ class MyText(Text): OBLIQUE = 'OBLIQUE' BOLD = 'BOLD' -HELP_MESSAGE = """ - Usage: - python extract_scene.py [] - -p preview in low quality - -s show and save picture of last frame - -w write result to file [this is default if nothing else is stated] - -o write to a different file_name - -l use low quality - -m use medium quality - -a run and save every scene in the script, or all args for the given scene - -q don't print progress - -f when writing to a movie file, export the frames in png sequence - -t use transperency when exporting images - -n specify the number of the animation to start from - -r specify a resolution - -c specify a background color -""" +TEX_USE_CTEX = False +TEX_TEXT_TO_REPLACE = "YourTextHere" +TEMPLATE_TEX_FILE = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "tex_template.tex" if not TEX_USE_CTEX else "ctex_template.tex" +) +with open(TEMPLATE_TEX_FILE, "r") as infile: + TEMPLATE_TEXT_FILE_BODY = infile.read() + TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace( + TEX_TEXT_TO_REPLACE, + "\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}", + ) + SCENE_NOT_FOUND_MESSAGE = """ {} is not in the script """ diff --git a/manim/mobject/svg/tex_mobject.py b/manim/mobject/svg/tex_mobject.py index f016cba804..4c5d645150 100644 --- a/manim/mobject/svg/tex_mobject.py +++ b/manim/mobject/svg/tex_mobject.py @@ -11,6 +11,7 @@ from ...utils.strings import split_string_list_to_isolate_substrings from ...utils.tex_file_writing import tex_to_svg_file + TEX_MOB_SCALE_FACTOR = 0.05 @@ -20,8 +21,10 @@ class TexSymbol(VMobjectFromSVGPathstring): """ pass + class SingleStringTexMobject(SVGMobject): CONFIG = { + "template_tex_file_body": TEMPLATE_TEX_FILE_BODY, "stroke_width": 0, "fill_opacity": 1.0, "background_stroke_width": 1, @@ -30,7 +33,6 @@ class SingleStringTexMobject(SVGMobject): "height": None, "organize_left_to_right": False, "alignment": "", - "type": "tex", } def __init__(self, tex_string, **kwargs): @@ -39,7 +41,7 @@ def __init__(self, tex_string, **kwargs): self.tex_string = tex_string file_name = tex_to_svg_file( self.get_modified_expression(tex_string), - self.type + self.template_tex_file_body ) SVGMobject.__init__(self, file_name=file_name, **kwargs) if self.height is None: @@ -245,9 +247,9 @@ def sort_alphabetically(self): class TextMobject(TexMobject): CONFIG = { + "template_tex_file_body": TEMPLATE_TEXT_FILE_BODY, "alignment": "\\centering", "arg_separator": "", - "type": "text", } @@ -256,6 +258,7 @@ class BulletedList(TextMobject): "buff": MED_LARGE_BUFF, "dot_scale_factor": 2, # Have to include because of handle_multiple_args implementation + "template_tex_file_body": TEMPLATE_TEXT_FILE_BODY, "alignment": "", } @@ -322,4 +325,4 @@ def __init__(self, *text_parts, **kwargs): else: underline.set_width(self.underline_width) self.add(underline) - self.underline = underline + self.underline = underline \ No newline at end of file diff --git a/manim/utils/tex.py b/manim/utils/tex.py deleted file mode 100644 index a38161b31b..0000000000 --- a/manim/utils/tex.py +++ /dev/null @@ -1,288 +0,0 @@ -import os -from ..utils.config_ops import digest_config - -class TexTemplateFromFile(): - """ - Class representing a TeX template file - """ # TODO: attributes, dataclasses stuff - CONFIG = { - "use_ctex": False, - "filename" : "tex_template.tex", - "text_to_replace": "YourTextHere", - } - body = "" - - def __init__(self, **kwargs): - digest_config(self, kwargs) - self.rebuild_cache() - - def rebuild_cache(self): - """For faster access, the LaTeX template's code is cached. - If the base file is modified, the cache needs to be rebuilt. - """ - with open(self.filename, "r") as infile: - self.body = infile.read() - - def get_text_for_text_mode(self,expression): - """Inserting expression verbatim into TeX template. - - Parameters - ---------- - expression : :class:`str` - String containing the expression to be typeset, e.g. `"foo"` - - Returns - ------- - :class:`str` - LaTeX code based on the template containing the given expression and ready for typesetting. - """ - return self.body.replace( - self.text_to_replace, expression - ) - - def get_text_for_env(self, environment, expression): - """Inserts an expression into the TeX template, surrounded - by `\\begin{} ... \\end{}` for a certain environment. - - Parameters - ---------- - environment : :class:`str` - The environment in which we should wrap the expression. - expression : :class:`str` - The string containing the expression to be typeset, e.g. $\\sqrt{2}$ - - Returns - ------- - :class:`str` - LaTeX code based on template, containing the given expression and ready for typesetting - """ - begin = r"\begin{" + environment + "}" - end = r"\end{" + environment + "}" - return self.body.replace( - self.text_to_replace, - "{0}\n{1}\n{2}".format(begin, expression, end) - ) - - def get_text_for_tex_mode(self,expression): - """Inserts an expression into the TeX template, surrounded - by `\\begin{align*} ... \\end{align*}` for math mode. - - Parameters - ---------- - expression : :class:`str` - The string containing the (math) expression to be typeset, e.g. $\\sqrt{2}$ - - Returns - ------- - :class:`str` - LaTeX code based on template, containing the given expression and ready for typesetting - """ - return self.get_text_for_env("align*", expression) - - -class TexTemplate(TexTemplateFromFile): - """ - Class for dynamically managing a TeX template - """ # TODO: Add attributes (when dataclasses are implemented) - CONFIG = { - "documentclass": ["standalone",["preview"]], - "common_packages": [ - ["babel",["english"]], - "amsmath", - "amssymb", - "dsfont", - "setspace", - "tipa", - "relsize", - "textcomp", - "mathrsfs", - "calligra", - "wasysym", - "ragged2e", - "physics", - "xcolor", - "microtype" - ], - "tex_packages": [], - "ctex_packages": [["ctex",["UTF8"]]], - "common_preamble_text": r"\linespread{1}" "\n", - "tex_preamble_text": r"\DisableLigatures{encoding = *, family = *}" "\n", - "ctex_preamble_text": "", - "document_prefix": "", - "document_suffix": "", - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - self.rebuild_cache() - - def rebuild_cache(self): - """For faster access, the LaTeX template's code is cached. - If the base file is modified, the cache needs to be rebuilt.""" - tpl = self.generate_tex_command( - "documentclass",required_params=[self.documentclass[0]], optional_params=self.documentclass[1] - ) - for pkg in self.common_packages: - tpl += self.generate_usepackage(pkg) - - if self.use_ctex: - for pkg in self.ctex_packages: - tpl += self.generate_usepackage(pkg) - else: - for pkg in self.tex_packages: - tpl += self.generate_usepackage(pkg) - - tpl += self.common_preamble_text - if self.use_ctex: - tpl += self.ctex_preamble_text - else: - tpl += self.tex_preamble_text - - tpl += "\n" r"\begin{document}" "\n" - tpl += f"\n{self.text_to_replace}\n" - tpl += "\n" r"\end{document}" - - self.body=tpl - - def prepend_package(self, pkg): - """Adds a new package (or several new packages) - before all other packages. Sometimes, the order of - the `\\usepackage` directives is relevant. - - Parameters - ---------- - pkg : :class:`str` - The package name, e.g. "siunitx" - """ - self.common_packages.insert(0, pkg) - self.rebuild_cache() - - def append_package(self, pkg): - """Adds a new package (or several new packages) - after all other packages. Sometimes, the order of - the `\\usepackage` directives is relevant. - - Parameters - ---------- - pkg : :class:`str` - The package name, e.g. "siunitx" - """ - self.common_packages.append(pkg) - self.rebuild_cache() - - def append_to_preamble(self,text): - """Adds commands (e.g. macro definitions) at the end of the preamble. - - Parameters - ---------- - text : :class:`str` - The text to be included, e.g. "\\newcommand{\\R}{\\mathbb{Q}}" - """ - if self.use_ctex: - self.ctex_preamble_text += text - else: - self.tex_preamble_text += text - self.rebuild_cache() - pass - - def clear_preamble(self): - """Removes custom definitions from the LaTeX preamble. - This does not affect the imported packages or documentclass.""" - self.common_preamble_text = "" - self.ctex_preamble_text = "" - self.tex_preamble_text = "" - self.rebuild_cache() - pass - - def generate_tex_command(self,command, *, required_params, optional_params = []): - """ - Function for creating LaTeX command strings with or without options. - Internally used to generate `\\usepackage{...}` - - Parameters - ---------- - command : :class:`str` - The command, e.g. `"usepackage"` - required_params : Iterable[:class:`str`] - The required parameters of this command, each wrapped in `{}`s. - optional_params : Iterable[:class:`str`] - The optional parameters of this command, each separated by a comma inside one `[]`. - - Examples - -------- - :: - generate_tex_command("usepackage", required_params=["packagename"], optional_params=["option1", "option2"]) - - Returns - ------- - :class:`str` - The generated command. - """ - optional_params = list(optional_params) # so we can measure its length - return r"\{0}{1}{2}".format( - command, - f"[{','.join(optional_params)}]" if optional_params else "", - "".join("{" + param + "}" for param in required_params) - ) - - def generate_usepackage(self,pkg): - if isinstance(pkg,list): - return self.generate_tex_command("usepackage",required_params=[pkg[0]],optional_params=pkg[1]) - else: - return self.generate_tex_command("usepackage",required_params=[pkg]) - - def get_text_for_text_mode(self,expression): - """Inserts an expression verbatim into the TeX template. - - Parameters - ----–----- - expression : :class:`str` - The expression to be typeset, e.g. `"foo"` - - Returns - ------- - :class:`str` - LaTeX code based on the template, containing the given expression and ready for typesetting - """ - return self.body.replace( - self.text_to_replace, expression - ) - - def get_text_for_env(self, environment, expression): - """Inserts an expression into the TeX template, surrounded - by `\\begin{} ... \\end{}` for a certain environment. - - Parameters - ---------- - environment : :class:`str` - The environment in which we should wrap the expression. - expression : :class:`str` - The string containing the expression to be typeset, e.g. $\\sqrt{2}$ - - Returns - ------- - :class:`str` - LaTeX code based on template, containing the given expression and ready for typesetting - """ - begin = r"\begin{" + environment + "}" - end = r"\end{" + environment + "}" - return self.body.replace( - self.text_to_replace, - "{0}\n{1}\n{2}".format(begin, expression, end) - ) - - def get_text_for_tex_mode(self,expression): - """Inserts an expression into the TeX template, surrounded - by `\\begin{align*} ... \\end{align*}` for math mode. - - Parameters - ---------- - expression : :class:`str` - The string containing the (math) expression to be typeset, e.g. $\\sqrt{2}$ - - Returns - ------- - :class:`str` - LaTeX code based on template, containing the given expression and ready for typesetting - """ - return self.get_text_for_env("align*", expression) diff --git a/manim/utils/tex_file_writing.py b/manim/utils/tex_file_writing.py index 8a28642dc2..e3951187d6 100644 --- a/manim/utils/tex_file_writing.py +++ b/manim/utils/tex_file_writing.py @@ -3,47 +3,44 @@ from pathlib import Path -#from ..constants import TEX_TEXT_TO_REPLACE -#from ..constants import TEX_USE_CTEX -import manim.constants as consts - +from ..constants import TEX_TEXT_TO_REPLACE +from ..constants import TEX_USE_CTEX from .. import dirs from ..logger import logger -def tex_hash(expression): - id_str = str(expression) +def tex_hash(expression, template_tex_file_body): + id_str = str(expression + template_tex_file_body) hasher = hashlib.sha256() hasher.update(id_str.encode()) # Truncating at 16 bytes for cleanliness return hasher.hexdigest()[:16] -def tex_to_svg_file(expression, source_type): - tex_template = consts.TEX_TEMPLATE - tex_file = generate_tex_file(expression, tex_template, source_type) - dvi_file = tex_to_dvi(tex_file, tex_template.use_ctex) - return dvi_to_svg(dvi_file, use_ctex=tex_template.use_ctex) -def generate_tex_file(expression, tex_template, source_type): - if source_type == "text": - output = tex_template.get_text_for_text_mode(expression) - elif source_type == "tex": - output = tex_template.get_text_for_tex_mode(expression) +def tex_to_svg_file(expression, template_tex_file_body): + tex_file = generate_tex_file(expression, template_tex_file_body) + dvi_file = tex_to_dvi(tex_file) + return dvi_to_svg(dvi_file) + +def generate_tex_file(expression, template_tex_file_body): result = os.path.join( dirs.TEX_DIR, - tex_hash(output) + tex_hash(expression, template_tex_file_body) ) + ".tex" if not os.path.exists(result): logger.info("Writing \"%s\" to %s" % ( "".join(expression), result )) + new_body = template_tex_file_body.replace( + TEX_TEXT_TO_REPLACE, expression + ) with open(result, "w", encoding="utf-8") as outfile: - outfile.write(output) + outfile.write(new_body) return result -def tex_to_dvi(tex_file, use_ctex = False): - result = tex_file.replace(".tex", ".dvi" if not use_ctex else ".xdv") +def tex_to_dvi(tex_file): + result = tex_file.replace(".tex", ".dvi" if not TEX_USE_CTEX else ".xdv") result = Path(result).as_posix() tex_file = Path(tex_file).as_posix() tex_dir = Path(dirs.TEX_DIR).as_posix() @@ -56,7 +53,7 @@ def tex_to_dvi(tex_file, use_ctex = False): "\"{}\"".format(tex_file), ">", os.devnull - ] if not use_ctex else [ + ] if not TEX_USE_CTEX else [ "xelatex", "-no-pdf", "-interaction=batchmode", @@ -70,20 +67,20 @@ def tex_to_dvi(tex_file, use_ctex = False): if exit_code != 0: log_file = tex_file.replace(".tex", ".log") raise Exception( - ("LaTeX error converting to dvi. " if not use_ctex - else "XeLaTeX error converting to xdv. ") + + ("Latex error converting to dvi. " if not TEX_USE_CTEX + else "Xelatex error converting to xdv. ") + "See log output above or the log file: %s" % log_file) return result -def dvi_to_svg(dvi_file, use_ctex=False, regen_if_exists=False): +def dvi_to_svg(dvi_file, regen_if_exists=False): """ 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 """ - result = dvi_file.replace(".dvi" if not use_ctex else ".xdv", ".svg") + result = dvi_file.replace(".dvi" if not TEX_USE_CTEX else ".xdv", ".svg") result = Path(result).as_posix() dvi_file = Path(dvi_file).as_posix() if not os.path.exists(result):