3b1b-manim/manimlib/mobject/svg/text_mobject.py

303 lines
9.2 KiB
Python
Raw Normal View History

2022-02-26 20:31:26 +08:00
import itertools as it
2021-02-12 12:28:08 +05:30
import os
import re
import typing
2022-02-26 20:31:26 +08:00
import xml.sax.saxutils as saxutils
2021-02-12 12:28:08 +05:30
from contextlib import contextmanager
from pathlib import Path
2022-02-26 20:31:26 +08:00
import pygments
import pygments.formatters
import pygments.lexers
import manimglpango
2022-01-26 19:55:47 +08:00
from manimlib.logger import log
2019-08-05 22:53:15 +08:00
from manimlib.constants import *
2021-01-18 08:13:18 -10:00
from manimlib.mobject.geometry import Dot
2019-08-05 22:53:15 +08:00
from manimlib.mobject.svg.svg_mobject import SVGMobject
2022-02-26 20:31:26 +08:00
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.tex_file_writing import tex_hash
2019-08-12 09:35:05 +08:00
from manimlib.utils.config_ops import digest_config
2022-02-26 20:31:26 +08:00
from manimlib.utils.directories import get_downloads_dir
from manimlib.utils.directories import get_text_dir
2019-08-05 22:53:15 +08:00
2021-06-19 19:33:53 +08:00
TEXT_MOB_SCALE_FACTOR = 0.0076
2021-10-01 12:32:38 -07:00
DEFAULT_LINE_SPACING_SCALE = 0.6
2021-09-15 20:02:57 +08:00
2019-08-05 22:53:15 +08:00
class Text(SVGMobject):
CONFIG = {
2019-08-12 14:40:42 +08:00
# Mobject
2022-02-26 20:31:26 +08:00
"svg_default": {
"color": WHITE,
"opacity": 1.0,
"stroke_width": 0,
},
"height": None,
2019-08-12 14:40:42 +08:00
# Text
"font_size": 48,
2022-02-26 20:31:26 +08:00
"lsh": None,
"justify": False,
"indent": 0,
"alignment": "LEFT",
"line_width": -1, # No auto wrapping if set to -1
"font": "",
"gradient": None,
"slant": NORMAL,
"weight": NORMAL,
"t2c": {},
"t2f": {},
"t2g": {},
"t2s": {},
"t2w": {},
2021-02-12 12:28:08 +05:30
"disable_ligatures": True,
2022-02-26 20:31:26 +08:00
"escape_chars": True,
"apply_space_chars": True,
2019-08-05 22:53:15 +08:00
}
2021-09-15 20:02:57 +08:00
def __init__(self, text, **kwargs):
self.full2short(kwargs)
digest_config(self, kwargs)
2022-02-26 20:31:26 +08:00
validate_error = manimglpango.validate(text)
if validate_error:
raise ValueError(validate_error)
self.text = text
2022-02-26 20:31:26 +08:00
super.__init__(**kwargs)
self.scale(self.font_size / 48) # TODO
2019-08-12 09:35:05 +08:00
if self.gradient:
self.set_color_by_gradient(*self.gradient)
2019-08-12 14:40:42 +08:00
# anti-aliasing
if self.height is None:
self.scale(TEXT_MOB_SCALE_FACTOR)
2022-02-26 20:31:26 +08:00
@property
def hash_seed(self):
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.text,
#self.font_size,
self.lsh,
self.justify,
self.indent,
self.alignment,
self.line_width,
self.font,
self.slant,
self.weight,
self.t2c,
self.t2f,
self.t2s,
self.t2w,
self.disable_ligatures,
self.escape_chars,
self.apply_space_chars
)
2021-03-18 17:34:57 -07:00
2022-02-26 20:31:26 +08:00
def get_file_path(self):
full_markup = self.get_full_markup_str()
svg_file = os.path.join(
get_text_dir(), tex_hash(full_markup) + ".svg"
)
if not os.path.exists(svg_file):
self.markup_to_svg(full_markup, svg_file)
return svg_file
def get_full_markup_str(self):
if self.t2g:
log.warning(
"Manim currently cannot parse gradient from svg. "
"Please set gradient via `set_color_by_gradient`.",
)
global_params = {}
lsh = self.lsh or DEFAULT_LINE_SPACING_SCALE
global_params["line_height"] = 0.6 * lsh + 0.64
if self.font:
global_params["font_family"] = self.font
#global_params["font_size"] = self.font_size * 1024
global_params["font_style"] = self.slant
global_params["font_weight"] = self.weight
if self.disable_ligatures:
global_params["font_features"] = "liga=0,dlig=0,clig=0,hlig=0"
text_span_to_params_map = {
(0, len(self.text)): global_params
}
for t2x_dict, key in (
(self.t2c, "color"),
(self.t2f, "font_family"),
(self.t2s, "font_style"),
(self.t2w, "font_weight")
):
for word_or_text_span, value in t2x_dict.items():
for text_span in self.find_indexes(word_or_text_span):
if text_span not in text_span_to_params_map:
text_span_to_params_map[text_span] = {}
text_span_to_params_map[text_span][key] = value
indices, _, flags, param_dicts = zip(*sorted([
(*text_span[::(1, -1)[flag]], flag, param_dict)
for text_span, param_dict in text_span_to_params_map.items()
for flag in range(2)
]))
tag_pieces = [
(f"<span {self.get_attr_list_str(param_dict)}>", "</span>")[flag]
for flag, param_dict in zip(flags, param_dicts)
]
tag_pieces.insert(0, "")
string_pieces = [
self.text[slice(*piece_span)]
for piece_span in list(adjacent_pairs(indices))[:-1]
]
if self.escape_chars:
string_pieces = list(map(saxutils.escape, string_pieces))
return "".join(it.chain(*zip(tag_pieces, string_pieces)))
def find_indexes(self, word_or_text_span):
if isinstance(word_or_text_span, tuple):
return [word_or_text_span]
return [
match_obj.span()
for match_obj in re.finditer(re.escape(word_or_text_span), self.text)
]
@staticmethod
def get_attr_list_str(param_dict):
return " ".join([
f"{key}='{value}'"
for key, value in param_dict.items()
])
def markup_to_svg(self, markup_str, file_name):
width = DEFAULT_PIXEL_WIDTH
height = DEFAULT_PIXEL_HEIGHT
2022-02-26 20:31:26 +08:00
justify = self.justify
indent = self.indent
alignment = ["LEFT", "CENTER", "RIGHT"].index(self.alignment.upper())
line_width = self.line_width * 1024
return manimglpango.markup_to_svg(
markup_str,
2021-02-12 12:28:08 +05:30
file_name,
width,
height,
2022-02-26 20:31:26 +08:00
justify=justify,
indent=indent,
alignment=alignment,
line_width=line_width
2021-02-12 12:28:08 +05:30
)
2022-02-26 20:31:26 +08:00
def generate_mobject(self):
super().generate_mobject()
2021-03-18 17:34:57 -07:00
2022-02-26 20:31:26 +08:00
# Remove empty paths
submobjects = list(filter(lambda submob: submob.has_points(), self))
2021-09-15 20:02:57 +08:00
2022-02-26 20:31:26 +08:00
# Apply space characters
if self.apply_space_chars:
for char_index, char in enumerate(self.text):
if not re.match(r"\s", char):
continue
space = Dot(radius=0, fill_opacity=0, stroke_opacity=0)
space.move_to(submobjects[max(char_index - 1, 0)].get_center())
submobjects.insert(char_index, space)
self.set_submobjects(submobjects)
2021-09-15 20:02:57 +08:00
2022-02-26 20:31:26 +08:00
def full2short(self, config):
conversion_dict = {
"line_spacing_height": "lsh",
"text2color": "t2c",
"text2font": "t2f",
"text2gradient": "t2g",
"text2slant": "t2s",
"text2weight": "t2w"
}
for kwargs in [config, self.CONFIG]:
for long_name, short_name in conversion_dict.items():
if long_name in kwargs:
kwargs[short_name] = kwargs.pop(long_name)
2022-02-26 20:31:26 +08:00
class MarkupText(Text):
CONFIG = {
"escape_chars": False,
"apply_space_chars": False,
}
2021-09-15 20:02:57 +08:00
class Code(Text):
CONFIG = {
"font": "Consolas",
"font_size": 24,
2022-02-26 20:31:26 +08:00
"lsh": 1.0, # TODO
2021-09-15 20:02:57 +08:00
"language": "python",
# Visit https://pygments.org/demo/ to have a preview of more styles.
2022-02-26 20:31:26 +08:00
"code_style": "monokai"
2021-09-15 20:02:57 +08:00
}
def __init__(self, code, **kwargs):
digest_config(self, kwargs)
2022-02-26 20:31:26 +08:00
self.code = code
2021-09-15 20:02:57 +08:00
lexer = pygments.lexers.get_lexer_by_name(self.language)
2022-02-26 20:31:26 +08:00
formatter = pygments.formatters.PangoMarkupFormatter(style=self.code_style)
markup_code = pygments.highlight(code, lexer, formatter)
super().__init__(markup_code, **kwargs)
2021-09-15 20:02:57 +08:00
2021-02-12 12:28:08 +05:30
@contextmanager
def register_font(font_file: typing.Union[str, Path]):
"""Temporarily add a font file to Pango's search path.
This searches for the font_file at various places. The order it searches it described below.
1. Absolute path.
2. Downloads dir.
Parameters
----------
font_file :
The font file to add.
Examples
--------
Use ``with register_font(...)`` to add a font file to search
path.
.. code-block:: python
with register_font("path/to/font_file.ttf"):
a = Text("Hello", font="Custom Font Name")
Raises
------
FileNotFoundError:
If the font doesn't exists.
AttributeError:
If this method is used on macOS.
Notes
-----
This method of adding font files also works with :class:`CairoText`.
.. important ::
This method is available for macOS for ``ManimPango>=v0.2.3``. Using this
method with previous releases will raise an :class:`AttributeError` on macOS.
2021-02-12 12:28:08 +05:30
"""
input_folder = Path(get_downloads_dir()).parent.resolve()
possible_paths = [
Path(font_file),
input_folder / font_file,
]
for path in possible_paths:
path = path.resolve()
if path.exists():
file_path = path
break
else:
error = f"Can't find {font_file}." f"Tried these : {possible_paths}"
raise FileNotFoundError(error)
try:
2022-02-26 20:31:26 +08:00
assert manimglpango.register_font(str(file_path))
2021-02-12 12:28:08 +05:30
yield
finally:
2022-02-26 20:31:26 +08:00
manimglpango.unregister_font(str(file_path))