diff options
Diffstat (limited to 'src/silx/gui/plot/backends/glutils/GLText.py')
-rw-r--r-- | src/silx/gui/plot/backends/glutils/GLText.py | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/src/silx/gui/plot/backends/glutils/GLText.py b/src/silx/gui/plot/backends/glutils/GLText.py new file mode 100644 index 0000000..d6ae6fa --- /dev/null +++ b/src/silx/gui/plot/backends/glutils/GLText.py @@ -0,0 +1,287 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2014-2020 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################*/ +""" +This module provides minimalistic text support for OpenGL. +It provides Latin-1 (ISO8859-1) characters for one monospace font at one size. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "03/04/2017" + + +from collections import OrderedDict +import weakref + +import numpy + +from ...._glutils import font, gl, Context, Program, Texture +from .GLSupport import mat4Translate + + +# TODO: Font should be configurable by the main program: using mpl.rcParams? + + +class _Cache(object): + """LRU (Least Recent Used) cache. + + :param int maxsize: Maximum number of (key, value) pairs in the cache + :param callable callback: + Called when a (key, value) pair is removed from the cache. + It must take 2 arguments: key and value. + """ + + def __init__(self, maxsize=128, callback=None): + self._maxsize = int(maxsize) + self._callback = callback + self._cache = OrderedDict() + + def __contains__(self, item): + return item in self._cache + + def __getitem__(self, key): + if key in self._cache: + # Remove/add key from ordered dict to store last access info + value = self._cache.pop(key) + self._cache[key] = value + return value + else: + raise KeyError + + def __setitem__(self, key, value): + """Add a key, value pair to the cache. + + :param key: The key to set + :param value: The corresponding value + """ + if key not in self._cache and len(self._cache) >= self._maxsize: + removedKey, removedValue = self._cache.popitem(last=False) + if self._callback is not None: + self._callback(removedKey, removedValue) + self._cache[key] = value + + +# Text2D ###################################################################### + +LEFT, CENTER, RIGHT = 'left', 'center', 'right' +TOP, BASELINE, BOTTOM = 'top', 'baseline', 'bottom' +ROTATE_90, ROTATE_180, ROTATE_270 = 90, 180, 270 + + +class Text2D(object): + + _SHADERS = { + 'vertex': """ + #version 120 + + attribute vec2 position; + attribute vec2 texCoords; + uniform mat4 matrix; + + varying vec2 vCoords; + + void main(void) { + gl_Position = matrix * vec4(position, 0.0, 1.0); + vCoords = texCoords; + } + """, + 'fragment': """ + #version 120 + + uniform sampler2D texText; + uniform vec4 color; + uniform vec4 bgColor; + + varying vec2 vCoords; + + void main(void) { + gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r); + } + """ + } + + _TEX_COORDS = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)), + dtype=numpy.float32).ravel() + + _program = Program(_SHADERS['vertex'], + _SHADERS['fragment'], + attrib0='position') + + # Discard texture objects when removed from the cache + _textures = weakref.WeakKeyDictionary() + """Cache already created textures""" + + _sizes = _Cache() + """Cache already computed sizes""" + + def __init__(self, text, x=0, y=0, + color=(0., 0., 0., 1.), + bgColor=None, + align=LEFT, valign=BASELINE, + rotate=0, + devicePixelRatio= 1.): + self.devicePixelRatio = devicePixelRatio + self._vertices = None + self._text = text + self.x = x + self.y = y + self.color = color + self.bgColor = bgColor + + if align not in (LEFT, CENTER, RIGHT): + raise ValueError( + "Horizontal alignment not supported: {0}".format(align)) + self._align = align + + if valign not in (TOP, CENTER, BASELINE, BOTTOM): + raise ValueError( + "Vertical alignment not supported: {0}".format(valign)) + self._valign = valign + + self._rotate = numpy.radians(rotate) + + def _getTexture(self, text, devicePixelRatio): + # Retrieve/initialize texture cache for current context + textureKey = text, devicePixelRatio + + context = Context.getCurrent() + if context not in self._textures: + self._textures[context] = _Cache( + callback=lambda key, value: value[0].discard()) + textures = self._textures[context] + + if textureKey not in textures: + image, offset = font.rasterText( + text, + font.getDefaultFontFamily(), + devicePixelRatio=self.devicePixelRatio) + if textureKey not in self._sizes: + self._sizes[textureKey] = image.shape[1], image.shape[0] + + texture = Texture( + gl.GL_RED, + data=image, + minFilter=gl.GL_NEAREST, + magFilter=gl.GL_NEAREST, + wrap=(gl.GL_CLAMP_TO_EDGE, + gl.GL_CLAMP_TO_EDGE)) + texture.prepare() + textures[textureKey] = texture, offset + + return textures[textureKey] + + @property + def text(self): + return self._text + + @property + def size(self): + textureKey = self.text, self.devicePixelRatio + if textureKey not in self._sizes: + image, offset = font.rasterText( + self.text, + font.getDefaultFontFamily(), + devicePixelRatio=self.devicePixelRatio) + self._sizes[textureKey] = image.shape[1], image.shape[0] + return self._sizes[textureKey] + + def getVertices(self, offset, shape): + height, width = shape + + if self._align == LEFT: + xOrig = 0 + elif self._align == RIGHT: + xOrig = - width + else: # CENTER + xOrig = - width // 2 + + if self._valign == BASELINE: + yOrig = - offset + elif self._valign == TOP: + yOrig = 0 + elif self._valign == BOTTOM: + yOrig = - height + else: # CENTER + yOrig = - height // 2 + + vertices = numpy.array(( + (xOrig, yOrig), + (xOrig + width, yOrig), + (xOrig, yOrig + height), + (xOrig + width, yOrig + height)), dtype=numpy.float32) + + cos, sin = numpy.cos(self._rotate), numpy.sin(self._rotate) + vertices = numpy.ascontiguousarray(numpy.transpose(numpy.array(( + cos * vertices[:, 0] - sin * vertices[:, 1], + sin * vertices[:, 0] + cos * vertices[:, 1]), + dtype=numpy.float32))) + + return vertices + + def render(self, matrix): + if not self.text: + return + + prog = self._program + prog.use() + + texUnit = 0 + texture, offset = self._getTexture(self.text, self.devicePixelRatio) + + gl.glUniform1i(prog.uniforms['texText'], texUnit) + + mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y))) + gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE, + mat.astype(numpy.float32)) + + gl.glUniform4f(prog.uniforms['color'], *self.color) + if self.bgColor is not None: + bgColor = self.bgColor + else: + bgColor = self.color[0], self.color[1], self.color[2], 0. + gl.glUniform4f(prog.uniforms['bgColor'], *bgColor) + + vertices = self.getVertices(offset, texture.shape) + + posAttrib = prog.attributes['position'] + gl.glEnableVertexAttribArray(posAttrib) + gl.glVertexAttribPointer(posAttrib, + 2, + gl.GL_FLOAT, + gl.GL_FALSE, + 0, + vertices) + + texAttrib = prog.attributes['texCoords'] + gl.glEnableVertexAttribArray(texAttrib) + gl.glVertexAttribPointer(texAttrib, + 2, + gl.GL_FLOAT, + gl.GL_FALSE, + 0, + self._TEX_COORDS) + + with texture: + gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4) |