Created
December 4, 2020 08:54
-
-
Save naveen521kk/56e430baa2786f36357ad92deb6a0709 to your computer and use it in GitHub Desktop.
This is a small extension for Manim which will allow use of ttf files for rendering fonts in Manim using text2svg library.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# By Naveen M K | |
from manim import * | |
from text2svg import TextInfo, register_font, Style, Weight,CharSettings,text2svg | |
import os | |
import hashlib | |
import re | |
TEXT_MOB_SCALE_FACTOR = 0.05 | |
class Text2(SVGMobject): | |
def __init__( | |
self, | |
text: str, | |
fill_opacity=1, | |
stroke_width=0, | |
color=WHITE, | |
size=1, | |
line_spacing=-1, | |
font="", | |
slant=NORMAL, | |
weight=NORMAL, | |
t2c=None, | |
t2f=None, | |
t2g=None, | |
t2s=None, | |
t2w=None, | |
gradient=None, | |
tab_width=4, | |
# Mobject | |
height=None, | |
width=None, | |
should_center=True, | |
unpack_groups=True, | |
**kwargs, | |
): | |
logger.info( | |
"Text now uses Pango for rendering. " | |
"In case of problems, the old implementation is available as CairoText." | |
) | |
self.size = size | |
self.line_spacing = line_spacing | |
self.font = font | |
self.slant = slant | |
self.weight = weight | |
self.gradient = gradient | |
self.tab_width = tab_width | |
if t2c is None: | |
t2c = {} | |
if t2f is None: | |
t2f = {} | |
if t2g is None: | |
t2g = {} | |
if t2s is None: | |
t2s = {} | |
if t2w is None: | |
t2w = {} | |
# If long form arguments are present, they take precedence | |
t2c = kwargs.pop("text2color", t2c) | |
t2f = kwargs.pop("text2font", t2f) | |
t2g = kwargs.pop("text2gradient", t2g) | |
t2s = kwargs.pop("text2slant", t2s) | |
t2w = kwargs.pop("text2weight", t2w) | |
self.t2c = t2c | |
self.t2f = t2f | |
self.t2g = t2g | |
self.t2s = t2s | |
self.t2w = t2w | |
self.original_text = text | |
text_without_tabs = text | |
if text.find("\t") != -1: | |
text_without_tabs = text.replace("\t", " " * self.tab_width) | |
self.text = text_without_tabs | |
if self.line_spacing == -1: | |
self.line_spacing = self.size + self.size * 0.3 | |
else: | |
self.line_spacing = self.size + self.size * self.line_spacing | |
file_name = self.text2svg() | |
self.remove_last_M(file_name) | |
SVGMobject.__init__( | |
self, | |
file_name, | |
color=color, | |
fill_opacity=fill_opacity, | |
stroke_width=stroke_width, | |
height=height, | |
width=width, | |
should_center=should_center, | |
unpack_groups=unpack_groups, | |
**kwargs, | |
) | |
self.text = text | |
self.chars = VGroup(*self.submobjects) | |
self.text = text_without_tabs.replace(" ", "").replace("\n", "") | |
nppc = self.n_points_per_cubic_curve | |
for each in self: | |
if len(each.points) == 0: | |
continue | |
points = each.points | |
last = points[0] | |
each.clear_points() | |
for index, point in enumerate(points): | |
each.append_points([point]) | |
if ( | |
index != len(points) - 1 | |
and (index + 1) % nppc == 0 | |
and any(point != points[index + 1]) | |
): | |
each.add_line_to(last) | |
last = points[index + 1] | |
each.add_line_to(last) | |
if self.t2c: | |
self.set_color_by_t2c() | |
if self.gradient: | |
self.set_color_by_gradient(*self.gradient) | |
if self.t2g: | |
self.set_color_by_t2g() | |
# anti-aliasing | |
if self.height is None and self.width is None: | |
self.scale(TEXT_MOB_SCALE_FACTOR) | |
def __repr__(self): | |
return f"Text({repr(self.original_text)})" | |
def remove_last_M(self, file_name: str): # pylint: disable=invalid-name | |
"""Internally used. Use to format the rendered SVG files.""" | |
with open(file_name, "r") as fpr: | |
content = fpr.read() | |
content = re.sub(r'Z M [^A-Za-z]*? "\/>', 'Z "/>', content) | |
with open(file_name, "w") as fpw: | |
fpw.write(content) | |
def find_indexes(self, word: str, text: str): | |
"""Internally used function. Finds the indexes of ``text`` in ``word``.""" | |
temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word) | |
if temp: | |
start = int(temp.group(1)) if temp.group(1) != "" else 0 | |
end = int(temp.group(2)) if temp.group(2) != "" else len(text) | |
start = len(text) + start if start < 0 else start | |
end = len(text) + end if end < 0 else end | |
return [(start, end)] | |
indexes = [] | |
index = text.find(word) | |
while index != -1: | |
indexes.append((index, index + len(word))) | |
index = text.find(word, index + len(word)) | |
return indexes | |
def set_color_by_t2c(self, t2c=None): | |
"""Internally used function. Sets colour for specified strings.""" | |
t2c = t2c if t2c else self.t2c | |
for word, color in list(t2c.items()): | |
for start, end in self.find_indexes(word, self.original_text): | |
self.chars[start:end].set_color(color) | |
def set_color_by_t2g(self, t2g=None): | |
"""Internally used. Sets gradient colors for specified | |
strings. Behaves similarly to ``set_color_by_t2c``.""" | |
t2g = t2g if t2g else self.t2g | |
for word, gradient in list(t2g.items()): | |
for start, end in self.find_indexes(word, self.original_text): | |
self.chars[start:end].set_color_by_gradient(*gradient) | |
def str2style(self, string): | |
"""Internally used function. Converts text to Pango Understandable Styles.""" | |
if string == NORMAL: | |
return Style.NORMAL | |
elif string == ITALIC: | |
return Style.ITALIC | |
elif string == OBLIQUE: | |
return Style.OBLIQUE | |
else: | |
raise AttributeError("There is no Style Called %s" % string) | |
def str2weight(self, string): | |
"""Internally used function. Convert text to Pango Understandable Weight""" | |
if string == NORMAL: | |
return Weight.NORMAL | |
elif string == BOLD: | |
return Weight.BOLD | |
elif string == THIN: | |
return Weight.THIN | |
elif string == ULTRALIGHT: | |
return Weight.ULTRALIGHT | |
elif string == LIGHT: | |
return Weight.LIGHT | |
elif string == SEMILIGHT: | |
return Weight.SEMILIGHT | |
elif string == BOOK: | |
return Weight.BOOK | |
elif string == MEDIUM: | |
return Weight.MEDIUM | |
elif string == SEMIBOLD: | |
return Weight.SEMIBOLD | |
elif string == ULTRABOLD: | |
return Weight.ULTRABOLD | |
elif string == HEAVY: | |
return Weight.HEAVY | |
elif string == ULTRAHEAVY: | |
return Weight.ULTRAHEAVY | |
else: | |
raise AttributeError("There is no Font Weight Called %s" % string) | |
def text2hash(self): | |
"""Internally used function. | |
Generates ``sha256`` hash for file name. | |
""" | |
settings = ( | |
"PANGO" + str(self.font) + str(self.slant) + str(self.weight) | |
) # to differentiate Text and CairoText | |
settings += str(self.t2f) + str(self.t2s) + str(self.t2w) | |
settings += str(self.line_spacing) + str(self.size) | |
id_str = self.text + settings | |
hasher = hashlib.sha256() | |
hasher.update(id_str.encode()) | |
return hasher.hexdigest()[:16] | |
@property | |
def slant(self): | |
return self._slant | |
@slant.setter | |
def slant(self,slant): | |
self._slant= self.str2style(slant) | |
@property | |
def weight(self): | |
return self._weight | |
@weight.setter | |
def weight(self,weight): | |
self._weight= self.str2weight(weight) | |
def text2settings(self): | |
"""Internally used function. Converts the texts and styles | |
to a setting for parsing.""" | |
settings = [] | |
t2x = [self.t2f, self.t2s, self.t2w] | |
for i in range(len(t2x)): | |
fsw = [self.font, self.slant, self.weight] | |
if t2x[i]: | |
for word, x in list(t2x[i].items()): | |
for start, end in self.find_indexes(word, self.text): | |
fsw[i] = x | |
settings.append(CharSettings(start, end, *fsw)) | |
# Set all text settings (default font, slant, weight) | |
fsw = [self.font, self.slant, self.weight] | |
settings.sort(key=lambda setting: setting.start) | |
temp_settings = settings.copy() | |
start = 0 | |
for setting in settings: | |
if setting.start != start: | |
temp_settings.append(CharSettings(start, setting.start, *fsw)) | |
start = setting.end | |
if start != len(self.text): | |
temp_settings.append(CharSettings(start, len(self.text), *fsw)) | |
settings = sorted(temp_settings, key=lambda setting: setting.start) | |
return settings | |
def text2svg(self): | |
"""Internally used function. | |
Convert the text to SVG using Pango | |
""" | |
size = self.size * 10 | |
dir_name = config.get_dir("text_dir") | |
if not os.path.exists(dir_name): | |
os.makedirs(dir_name) | |
hash_name = self.text2hash() | |
file_name = os.path.join(dir_name, hash_name) + ".svg" | |
if os.path.exists(file_name): | |
return file_name | |
style = self.slant | |
weight = self.weight | |
settings = self.text2settings() | |
a = TextInfo( | |
self.text, | |
file_name, | |
600, | |
400, | |
size, | |
style, | |
weight, | |
font=self.font, | |
START_X=START_X, | |
START_Y=START_Y, | |
text_setting = settings | |
) | |
text2svg(a) | |
return file_name | |
class Some(Scene): | |
def construct(self): | |
register_font(r"D:\Tangerine-Regular.ttf") | |
a = Text2("Some", font="Tangerine",t2c={"om":RED}) | |
self.play(Write(a)) | |
self.wait(2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment