OpenMotionLab/MotionGPT
118
1"""Font texture loader and processor.2 3Author: Matthew Matl4"""5import freetype6import numpy as np7import os8 9import OpenGL10from OpenGL.GL import *11 12from .constants import TextAlign, FLOAT_SZ13from .texture import Texture14from .sampler import Sampler15 16 17class FontCache(object):18 """A cache for fonts.19 """20 21 def __init__(self, font_dir=None):22 self._font_cache = {}23 self.font_dir = font_dir24 if self.font_dir is None:25 base_dir, _ = os.path.split(os.path.realpath(__file__))26 self.font_dir = os.path.join(base_dir, 'fonts')27 28 def get_font(self, font_name, font_pt):29 # If it's a file, load it directly, else, try to load from font dir.30 if os.path.isfile(font_name):31 font_filename = font_name32 _, font_name = os.path.split(font_name)33 font_name, _ = os.path.split(font_name)34 else:35 font_filename = os.path.join(self.font_dir, font_name) + '.ttf'36 37 cid = OpenGL.contextdata.getContext()38 key = (cid, font_name, int(font_pt))39 40 if key not in self._font_cache:41 self._font_cache[key] = Font(font_filename, font_pt)42 return self._font_cache[key]43 44 def clear(self):45 for key in self._font_cache:46 self._font_cache[key].delete()47 self._font_cache = {}48 49 50class Character(object):51 """A single character, with its texture and attributes.52 """53 54 def __init__(self, texture, size, bearing, advance):55 self.texture = texture56 self.size = size57 self.bearing = bearing58 self.advance = advance59 60 61class Font(object):62 """A font object.63 64 Parameters65 ----------66 font_file : str67 The file to load the font from.68 font_pt : int69 The height of the font in pixels.70 """71 72 def __init__(self, font_file, font_pt=40):73 self.font_file = font_file74 self.font_pt = int(font_pt)75 self._face = freetype.Face(font_file)76 self._face.set_pixel_sizes(0, font_pt)77 self._character_map = {}78 79 for i in range(0, 128):80 81 # Generate texture82 face = self._face83 face.load_char(chr(i))84 buf = face.glyph.bitmap.buffer85 src = (np.array(buf) / 255.0).astype(np.float32)86 src = src.reshape((face.glyph.bitmap.rows,87 face.glyph.bitmap.width))88 tex = Texture(89 sampler=Sampler(90 magFilter=GL_LINEAR,91 minFilter=GL_LINEAR,92 wrapS=GL_CLAMP_TO_EDGE,93 wrapT=GL_CLAMP_TO_EDGE94 ),95 source=src,96 source_channels='R',97 )98 character = Character(99 texture=tex,100 size=np.array([face.glyph.bitmap.width,101 face.glyph.bitmap.rows]),102 bearing=np.array([face.glyph.bitmap_left,103 face.glyph.bitmap_top]),104 advance=face.glyph.advance.x105 )106 self._character_map[chr(i)] = character107 108 self._vbo = None109 self._vao = None110 111 @property112 def font_file(self):113 """str : The file the font was loaded from.114 """115 return self._font_file116 117 @font_file.setter118 def font_file(self, value):119 self._font_file = value120 121 @property122 def font_pt(self):123 """int : The height of the font in pixels.124 """125 return self._font_pt126 127 @font_pt.setter128 def font_pt(self, value):129 self._font_pt = int(value)130 131 def _add_to_context(self):132 133 self._vao = glGenVertexArrays(1)134 glBindVertexArray(self._vao)135 self._vbo = glGenBuffers(1)136 glBindBuffer(GL_ARRAY_BUFFER, self._vbo)137 glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, None, GL_DYNAMIC_DRAW)138 glEnableVertexAttribArray(0)139 glVertexAttribPointer(140 0, 4, GL_FLOAT, GL_FALSE, 4 * FLOAT_SZ, ctypes.c_void_p(0)141 )142 glBindVertexArray(0)143 144 glPixelStorei(GL_UNPACK_ALIGNMENT, 1)145 for c in self._character_map:146 ch = self._character_map[c]147 if not ch.texture._in_context():148 ch.texture._add_to_context()149 150 def _remove_from_context(self):151 for c in self._character_map:152 ch = self._character_map[c]153 ch.texture.delete()154 if self._vao is not None:155 glDeleteVertexArrays(1, [self._vao])156 glDeleteBuffers(1, [self._vbo])157 self._vao = None158 self._vbo = None159 160 def _in_context(self):161 return self._vao is not None162 163 def _bind(self):164 glBindVertexArray(self._vao)165 166 def _unbind(self):167 glBindVertexArray(0)168 169 def delete(self):170 self._unbind()171 self._remove_from_context()172 173 def render_string(self, text, x, y, scale=1.0,174 align=TextAlign.BOTTOM_LEFT):175 """Render a string to the current view buffer.176 177 Note178 ----179 Assumes correct shader program already bound w/ uniforms set.180 181 Parameters182 ----------183 text : str184 The text to render.185 x : int186 Horizontal pixel location of text.187 y : int188 Vertical pixel location of text.189 scale : int190 Scaling factor for text.191 align : int192 One of the TextAlign options which specifies where the ``x``193 and ``y`` parameters lie on the text. For example,194 :attr:`.TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate195 the position of the bottom-left corner of the textbox.196 """197 glActiveTexture(GL_TEXTURE0)198 glEnable(GL_BLEND)199 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)200 glDisable(GL_DEPTH_TEST)201 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)202 self._bind()203 204 # Determine width and height of text relative to x, y205 width = 0.0206 height = 0.0207 for c in text:208 ch = self._character_map[c]209 height = max(height, ch.bearing[1] * scale)210 width += (ch.advance >> 6) * scale211 212 # Determine offsets based on alignments213 xoff = 0214 yoff = 0215 if align == TextAlign.BOTTOM_RIGHT:216 xoff = -width217 elif align == TextAlign.BOTTOM_CENTER:218 xoff = -width / 2.0219 elif align == TextAlign.TOP_LEFT:220 yoff = -height221 elif align == TextAlign.TOP_RIGHT:222 yoff = -height223 xoff = -width224 elif align == TextAlign.TOP_CENTER:225 yoff = -height226 xoff = -width / 2.0227 elif align == TextAlign.CENTER:228 xoff = -width / 2.0229 yoff = -height / 2.0230 elif align == TextAlign.CENTER_LEFT:231 yoff = -height / 2.0232 elif align == TextAlign.CENTER_RIGHT:233 xoff = -width234 yoff = -height / 2.0235 236 x += xoff237 y += yoff238 239 ch = None240 for c in text:241 ch = self._character_map[c]242 xpos = x + ch.bearing[0] * scale243 ypos = y - (ch.size[1] - ch.bearing[1]) * scale244 w = ch.size[0] * scale245 h = ch.size[1] * scale246 247 vertices = np.array([248 [xpos, ypos, 0.0, 0.0],249 [xpos + w, ypos, 1.0, 0.0],250 [xpos + w, ypos + h, 1.0, 1.0],251 [xpos + w, ypos + h, 1.0, 1.0],252 [xpos, ypos + h, 0.0, 1.0],253 [xpos, ypos, 0.0, 0.0],254 ], dtype=np.float32)255 256 ch.texture._bind()257 258 glBindBuffer(GL_ARRAY_BUFFER, self._vbo)259 glBufferData(260 GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW261 )262 # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken263 # glBufferSubData(264 # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ,265 # np.ascontiguousarray(vertices.flatten)266 # )267 glDrawArrays(GL_TRIANGLES, 0, 6)268 x += (ch.advance >> 6) * scale269 270 self._unbind()271 if ch:272 ch.texture._unbind()273 