Skip to content

Instantly share code, notes, and snippets.

@naveen521kk
Created December 4, 2020 08:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naveen521kk/56e430baa2786f36357ad92deb6a0709 to your computer and use it in GitHub Desktop.
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.
# 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