Spaces:
Runtime error
Runtime error
"""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 | |
def font_file(self): | |
"""str : The file the font was loaded from. | |
""" | |
return self._font_file | |
def font_file(self, value): | |
self._font_file = value | |
def font_pt(self): | |
"""int : The height of the font in pixels. | |
""" | |
return self._font_pt | |
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() | |