|
| 1 | +import re |
| 2 | +import os |
| 3 | +import copy |
| 4 | +import hashlib |
| 5 | +import cairo |
| 6 | +import manimlib.constants as consts |
| 7 | +from manimlib.constants import * |
| 8 | +from manimlib.mobject.svg.svg_mobject import SVGMobject |
| 9 | +from manimlib.utils.config_ops import digest_config |
| 10 | + |
| 11 | + |
| 12 | +class TextSetting(object): |
| 13 | + def __init__(self, start, end, font, slant, weight, line_num=-1): |
| 14 | + self.start = start |
| 15 | + self.end = end |
| 16 | + self.font = font |
| 17 | + self.slant = slant |
| 18 | + self.weight = weight |
| 19 | + self.line_num = line_num |
| 20 | + |
| 21 | + |
| 22 | +class Text(SVGMobject): |
| 23 | + CONFIG = { |
| 24 | + # Mobject |
| 25 | + 'color': consts.WHITE, |
| 26 | + 'height': None, |
| 27 | + # Text |
| 28 | + 'font': '', |
| 29 | + 'gradient': None, |
| 30 | + 'lsh': -1, |
| 31 | + 'size': 1, |
| 32 | + 'slant': NORMAL, |
| 33 | + 'weight': NORMAL, |
| 34 | + 't2c': {}, |
| 35 | + 't2f': {}, |
| 36 | + 't2g': {}, |
| 37 | + 't2s': {}, |
| 38 | + 't2w': {}, |
| 39 | + } |
| 40 | + |
| 41 | + def __init__(self, text, **config): |
| 42 | + self.text = text |
| 43 | + self.full2short(config) |
| 44 | + digest_config(self, config) |
| 45 | + self.lsh = self.size if self.lsh == -1 else self.lsh |
| 46 | + |
| 47 | + file_name = self.text2svg() |
| 48 | + SVGMobject.__init__(self, file_name, **config) |
| 49 | + |
| 50 | + if self.t2c: |
| 51 | + self.set_color_by_t2c() |
| 52 | + if self.gradient: |
| 53 | + self.set_color_by_gradient(*self.gradient) |
| 54 | + if self.t2g: |
| 55 | + self.set_color_by_t2g() |
| 56 | + |
| 57 | + # anti-aliasing |
| 58 | + self.scale(0.1) |
| 59 | + |
| 60 | + def find_indexes(self, word): |
| 61 | + m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word) |
| 62 | + if m: |
| 63 | + start = int(m.group(1)) if m.group(1) != '' else 0 |
| 64 | + end = int(m.group(2)) if m.group(2) != '' else len(self.text) |
| 65 | + start = len(self.text) + start if start < 0 else start |
| 66 | + end = len(self.text) + end if end < 0 else end |
| 67 | + return [(start, end)] |
| 68 | + |
| 69 | + indexes = [] |
| 70 | + index = self.text.find(word) |
| 71 | + while index != -1: |
| 72 | + indexes.append((index, index + len(word))) |
| 73 | + index = self.text.find(word, index + len(word)) |
| 74 | + return indexes |
| 75 | + |
| 76 | + def full2short(self, config): |
| 77 | + for kwargs in [config, self.CONFIG]: |
| 78 | + if kwargs.__contains__('line_spacing_height'): |
| 79 | + kwargs['lsh'] = kwargs.pop('line_spacing_height') |
| 80 | + if kwargs.__contains__('text2color'): |
| 81 | + kwargs['t2c'] = kwargs.pop('text2color') |
| 82 | + if kwargs.__contains__('text2font'): |
| 83 | + kwargs['t2f'] = kwargs.pop('text2font') |
| 84 | + if kwargs.__contains__('text2gradient'): |
| 85 | + kwargs['t2g'] = kwargs.pop('text2gradient') |
| 86 | + if kwargs.__contains__('text2slant'): |
| 87 | + kwargs['t2s'] = kwargs.pop('text2slant') |
| 88 | + if kwargs.__contains__('text2weight'): |
| 89 | + kwargs['t2w'] = kwargs.pop('text2weight') |
| 90 | + |
| 91 | + def set_color_by_t2c(self, t2c=None): |
| 92 | + t2c = t2c if t2c else self.t2c |
| 93 | + for word, color in list(t2c.items()): |
| 94 | + for start, end in self.find_indexes(word): |
| 95 | + self[start:end].set_color(color) |
| 96 | + |
| 97 | + def set_color_by_t2g(self, t2g=None): |
| 98 | + t2g = t2g if t2g else self.t2g |
| 99 | + for word, gradient in list(t2g.items()): |
| 100 | + for start, end in self.find_indexes(word): |
| 101 | + self[start:end].set_color_by_gradient(*gradient) |
| 102 | + |
| 103 | + def str2slant(self, string): |
| 104 | + if string == NORMAL: |
| 105 | + return cairo.FontSlant.NORMAL |
| 106 | + if string == ITALIC: |
| 107 | + return cairo.FontSlant.ITALIC |
| 108 | + if string == OBLIQUE: |
| 109 | + return cairo.FontSlant.OBLIQUE |
| 110 | + |
| 111 | + def str2weight(self, string): |
| 112 | + if string == NORMAL: |
| 113 | + return cairo.FontWeight.NORMAL |
| 114 | + if string == BOLD: |
| 115 | + return cairo.FontWeight.BOLD |
| 116 | + |
| 117 | + def text2hash(self): |
| 118 | + settings = self.font + self.slant + self.weight |
| 119 | + settings += str(self.t2f) + str(self.t2s) + str(self.t2w) |
| 120 | + settings += str(self.lsh) + str(self.size) |
| 121 | + id_str = self.text+settings |
| 122 | + hasher = hashlib.sha256() |
| 123 | + hasher.update(id_str.encode()) |
| 124 | + return hasher.hexdigest()[:16] |
| 125 | + |
| 126 | + def text2settings(self): |
| 127 | + settings = [] |
| 128 | + t2x = [self.t2f, self.t2s, self.t2w] |
| 129 | + for i in range(len(t2x)): |
| 130 | + fsw = [self.font, self.slant, self.weight] |
| 131 | + if t2x[i]: |
| 132 | + for word, x in list(t2x[i].items()): |
| 133 | + for start, end in self.find_indexes(word): |
| 134 | + fsw[i] = x |
| 135 | + settings.append(TextSetting(start, end, *fsw)) |
| 136 | + |
| 137 | + # Set All text settings(default font slant weight) |
| 138 | + fsw = [self.font, self.slant, self.weight] |
| 139 | + settings.sort(key=lambda setting: setting.start) |
| 140 | + temp_settings = settings.copy() |
| 141 | + start = 0 |
| 142 | + for setting in settings: |
| 143 | + if setting.start != start: |
| 144 | + temp_settings.append(TextSetting(start, setting.start, *fsw)) |
| 145 | + start = setting.end |
| 146 | + if start != len(self.text): |
| 147 | + temp_settings.append(TextSetting(start, len(self.text), *fsw)) |
| 148 | + settings = sorted(temp_settings, key=lambda setting: setting.start) |
| 149 | + |
| 150 | + if re.search(r'\n', self.text): |
| 151 | + line_num = 0 |
| 152 | + for start, end in self.find_indexes('\n'): |
| 153 | + for setting in settings: |
| 154 | + if setting.line_num == -1: |
| 155 | + setting.line_num = line_num |
| 156 | + if start < setting.end: |
| 157 | + line_num += 1 |
| 158 | + new_setting = copy.copy(setting) |
| 159 | + setting.end = end |
| 160 | + new_setting.start = end |
| 161 | + new_setting.line_num = line_num |
| 162 | + settings.append(new_setting) |
| 163 | + settings.sort(key=lambda setting: setting.start) |
| 164 | + break |
| 165 | + |
| 166 | + for setting in settings: |
| 167 | + if setting.line_num == -1: |
| 168 | + setting.line_num = 0 |
| 169 | + |
| 170 | + return settings |
| 171 | + |
| 172 | + def text2svg(self): |
| 173 | + # anti-aliasing |
| 174 | + size = self.size * 10 |
| 175 | + lsh = self.lsh * 10 |
| 176 | + |
| 177 | + if self.font == '': |
| 178 | + print(NOT_SETTING_FONT_MSG) |
| 179 | + |
| 180 | + dir_name = consts.TEXT_DIR |
| 181 | + hash_name = self.text2hash() |
| 182 | + file_name = os.path.join(dir_name, hash_name)+'.svg' |
| 183 | + if os.path.exists(file_name): |
| 184 | + return file_name |
| 185 | + |
| 186 | + surface = cairo.SVGSurface(file_name, 600, 400) |
| 187 | + context = cairo.Context(surface) |
| 188 | + context.set_font_size(size) |
| 189 | + context.move_to(START_X, START_Y) |
| 190 | + |
| 191 | + settings = self.text2settings() |
| 192 | + offset_x = 0 |
| 193 | + last_line_num = 0 |
| 194 | + for setting in settings: |
| 195 | + font = setting.font |
| 196 | + slant = self.str2slant(setting.slant) |
| 197 | + weight = self.str2weight(setting.weight) |
| 198 | + text = self.text[setting.start:setting.end].replace('\n', ' ') |
| 199 | + |
| 200 | + context.select_font_face(font, slant, weight) |
| 201 | + if setting.line_num != last_line_num: |
| 202 | + offset_x = 0 |
| 203 | + last_line_num = setting.line_num |
| 204 | + context.move_to(START_X + offset_x, START_Y + lsh*setting.line_num) |
| 205 | + context.show_text(text) |
| 206 | + offset_x += context.text_extents(text)[4] |
| 207 | + |
| 208 | + return file_name |
0 commit comments