"""Font texture loader and processor. Author: Matthew Matl """ import freetype import numpy as np import os import OpenGL from OpenGL.GL import * from .constants import TextAlign, FLOAT_SZ from .texture import Texture from .sampler import Sampler class FontCache(object): """A cache for fonts. """ def __init__(self, font_dir=None): self._font_cache = {} self.font_dir = font_dir if self.font_dir is None: base_dir, _ = os.path.split(os.path.realpath(__file__)) self.font_dir = os.path.join(base_dir, 'fonts') def get_font(self, font_name, font_pt): # If it's a file, load it directly, else, try to load from font dir. if os.path.isfile(font_name): font_filename = font_name _, font_name = os.path.split(font_name) font_name, _ = os.path.split(font_name) else: font_filename = os.path.join(self.font_dir, font_name) + '.ttf' cid = OpenGL.contextdata.getContext() key = (cid, font_name, int(font_pt)) if key not in self._font_cache: self._font_cache[key] = Font(font_filename, font_pt) return self._font_cache[key] def clear(self): for key in self._font_cache: self._font_cache[key].delete() self._font_cache = {} class Character(object): """A single character, with its texture and attributes. """ def __init__(self, texture, size, bearing, advance): self.texture = texture self.size = size self.bearing = bearing self.advance = advance class Font(object): """A font object. Parameters ---------- font_file : str The file to load the font from. font_pt : int The height of the font in pixels. """ def __init__(self, font_file, font_pt=40): self.font_file = font_file self.font_pt = int(font_pt) self._face = freetype.Face(font_file) self._face.set_pixel_sizes(0, font_pt) self._character_map = {} for i in range(0, 128): # Generate texture face = self._face face.load_char(chr(i)) buf = face.glyph.bitmap.buffer src = (np.array(buf) / 255.0).astype(np.float32) src = src.reshape((face.glyph.bitmap.rows, face.glyph.bitmap.width)) tex = Texture( sampler=Sampler( magFilter=GL_LINEAR, minFilter=GL_LINEAR, wrapS=GL_CLAMP_TO_EDGE, wrapT=GL_CLAMP_TO_EDGE ), source=src, source_channels='R', ) character = Character( texture=tex, size=np.array([face.glyph.bitmap.width, face.glyph.bitmap.rows]), bearing=np.array([face.glyph.bitmap_left, face.glyph.bitmap_top]), advance=face.glyph.advance.x ) self._character_map[chr(i)] = character self._vbo = None self._vao = None @property def font_file(self): """str : The file the font was loaded from. """ return self._font_file @font_file.setter def font_file(self, value): self._font_file = value @property def font_pt(self): """int : The height of the font in pixels. """ return self._font_pt @font_pt.setter def font_pt(self, value): self._font_pt = int(value) def _add_to_context(self): self._vao = glGenVertexArrays(1) glBindVertexArray(self._vao) self._vbo = glGenBuffers(1) glBindBuffer(GL_ARRAY_BUFFER, self._vbo) glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, None, GL_DYNAMIC_DRAW) glEnableVertexAttribArray(0) glVertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, 4 * FLOAT_SZ, ctypes.c_void_p(0) ) glBindVertexArray(0) glPixelStorei(GL_UNPACK_ALIGNMENT, 1) for c in self._character_map: ch = self._character_map[c] if not ch.texture._in_context(): ch.texture._add_to_context() def _remove_from_context(self): for c in self._character_map: ch = self._character_map[c] ch.texture.delete() if self._vao is not None: glDeleteVertexArrays(1, [self._vao]) glDeleteBuffers(1, [self._vbo]) self._vao = None self._vbo = None def _in_context(self): return self._vao is not None def _bind(self): glBindVertexArray(self._vao) def _unbind(self): glBindVertexArray(0) def delete(self): self._unbind() self._remove_from_context() def render_string(self, text, x, y, scale=1.0, align=TextAlign.BOTTOM_LEFT): """Render a string to the current view buffer. Note ---- Assumes correct shader program already bound w/ uniforms set. Parameters ---------- text : str The text to render. x : int Horizontal pixel location of text. y : int Vertical pixel location of text. scale : int Scaling factor for text. align : int One of the TextAlign options which specifies where the ``x`` and ``y`` parameters lie on the text. For example, :attr:`.TextAlign.BOTTOM_LEFT` means that ``x`` and ``y`` indicate the position of the bottom-left corner of the textbox. """ glActiveTexture(GL_TEXTURE0) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glDisable(GL_DEPTH_TEST) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) self._bind() # Determine width and height of text relative to x, y width = 0.0 height = 0.0 for c in text: ch = self._character_map[c] height = max(height, ch.bearing[1] * scale) width += (ch.advance >> 6) * scale # Determine offsets based on alignments xoff = 0 yoff = 0 if align == TextAlign.BOTTOM_RIGHT: xoff = -width elif align == TextAlign.BOTTOM_CENTER: xoff = -width / 2.0 elif align == TextAlign.TOP_LEFT: yoff = -height elif align == TextAlign.TOP_RIGHT: yoff = -height xoff = -width elif align == TextAlign.TOP_CENTER: yoff = -height xoff = -width / 2.0 elif align == TextAlign.CENTER: xoff = -width / 2.0 yoff = -height / 2.0 elif align == TextAlign.CENTER_LEFT: yoff = -height / 2.0 elif align == TextAlign.CENTER_RIGHT: xoff = -width yoff = -height / 2.0 x += xoff y += yoff ch = None for c in text: ch = self._character_map[c] xpos = x + ch.bearing[0] * scale ypos = y - (ch.size[1] - ch.bearing[1]) * scale w = ch.size[0] * scale h = ch.size[1] * scale vertices = np.array([ [xpos, ypos, 0.0, 0.0], [xpos + w, ypos, 1.0, 0.0], [xpos + w, ypos + h, 1.0, 1.0], [xpos + w, ypos + h, 1.0, 1.0], [xpos, ypos + h, 0.0, 1.0], [xpos, ypos, 0.0, 0.0], ], dtype=np.float32) ch.texture._bind() glBindBuffer(GL_ARRAY_BUFFER, self._vbo) glBufferData( GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW ) # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken # glBufferSubData( # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ, # np.ascontiguousarray(vertices.flatten) # ) glDrawArrays(GL_TRIANGLES, 0, 6) x += (ch.advance >> 6) * scale self._unbind() if ch: ch.texture._unbind()