Source code for ccpn.ui.gui.lib.OpenGL.CcpnOpenGLFonts

"""
Module Documentation here
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2022"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2022-01-27 15:24:37 +0000 (Thu, January 27, 2022) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Ed Brooksbank $"
__date__ = "$Date: 2018-12-20 14:07:55 +0000 (Thu, December 20, 2018) $"
#=========================================================================================
# Start of code
#=========================================================================================

import sys, os
from imageio import imread
from PyQt5 import QtWidgets
import numpy as np
import math
from collections import namedtuple
from ccpn.ui.gui.lib.OpenGL.CcpnOpenGLArrays import GLVertexArray, GLRENDERMODE_DRAW
from ccpn.ui.gui.lib.OpenGL.CcpnOpenGLDefs import LEFTBORDER, RIGHTBORDER, TOPBORDER, BOTTOMBORDER
from ccpn.util.Colour import hexToRgbRatio
from ccpn.util.AttrDict import AttrDict

from ccpn.ui.gui.lib.OpenGL import GL, GLU, GLUT


GlyphXpos = 'Xpos'
GlyphYpos = 'Ypos'
GlyphWidth = 'Width'
GlyphHeight = 'Height'
GlyphXoffset = 'Xoffset'
GlyphYoffset = 'Yoffset'
GlyphOrigW = 'OrigW'
GlyphOrigH = 'OrigH'
GlyphKerns = 'Kerns'
GlyphTX0 = 'tx0'
GlyphTY0 = 'ty0'
GlyphTX1 = 'tx1'
GlyphTY1 = 'ty1'
GlyphPX0 = 'px0'
GlyphPY0 = 'py0'
GlyphPX1 = 'px1'
GlyphPY1 = 'py1'

FONT_FILE = 0
FULL_FONT_NAME = 1

GLGlyphTuple = namedtuple('GLGlyphTuple', 'GlyphXpos GlyphYpos GlyphWidth GlyphHeight '
                                          'GlyphXoffset GlyphYoffset GlyphOrigW GlyphOrigH GlyphKerns '
                                          'GlyphTX0 GlyphTY0 GlyphTX1 GlyphTY1 '
                                          'GlyphPX0 GlyphPY0 GlyphPX1 GlyphPY1')


[docs]class CcpnGLFont(): def __init__(self, fileName=None, base=0, fontTransparency=None, activeTexture=0, scale=None): self.fontName = None self.fontGlyph = {} #[None] * 256 self.base = base self.scale = scale if scale == None: raise Exception('scale must be defined for font %s ' % fileName) with open(fileName, 'r') as op: self.fontInfo = op.read().split('\n') # no checking yet self.fontFile = self.fontInfo[FONT_FILE].replace('textures: ', '') self.fontPNG = imread(fileName.filepath / self.fontFile) self._fontArray = np.array(self.fontPNG * (fontTransparency if fontTransparency is not None else 1.0), dtype=np.uint8)[:, :, 3] fontRows = [] fontID = () for ii, row in enumerate(self.fontInfo): if ii and row and row[0].isalpha(): # assume that this is a font name if not row.startswith('kerning'): fontID = (ii, row) else: fontID += (ii,) fontRows.append(fontID) fontRows.append((len(self.fontInfo) - 1, None, None)) for _font, _nextFont in zip(fontRows, fontRows[1:]): _startRow, _fontName, _kerningRow = _font _nextRow = _nextFont[0] self._buildFont(_fontName, _startRow, _kerningRow, _nextRow, scale, fontTransparency) _foundFonts = [glyph.fontName for glyph in self.fontGlyph.values()] if len(set(_foundFonts)) != 1: raise Exception('font file should only contain a single font type') self.fontName = _foundFonts[0] self.activeTexture = GL.GL_TEXTURE0 + activeTexture self.activeTextureNum = activeTexture # self._bindFontTexture # causing threading error on Windows? def _bindFontTexture(self): """Bind the texture to font MUST be called inside GL current context, i.e., after GL.makeCurrent or inside initializeGL, paintGL """ self.textureId = GL.glGenTextures(1) # GL.glEnable(GL.GL_TEXTURE_2D) GL.glActiveTexture(self.activeTexture) GL.glBindTexture(GL.GL_TEXTURE_2D, self.textureId) # need to map ALPHA-ALPHA and use the alpha channel (.w) in the shader GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_ALPHA, self._fontArray.shape[1], self._fontArray.shape[0], 0, GL.GL_ALPHA, GL.GL_UNSIGNED_BYTE, self._fontArray) # nearest is the quickest gl plotting and gives a slightly brighter image # GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR) # GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR) GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST) GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST) # the following 2 lines generate a multitexture mipmap - shouldn't need here # GL.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_LINEAR ) # GL.glGenerateMipmap( GL.GL_TEXTURE_2D ) GL.glDisable(GL.GL_TEXTURE_2D) def _buildFont(self, _fontID, _startRow, _kerningRow, _nextRow, scale, fontTransparency): fullFontNameString = _fontID fontSizeString = fullFontNameString.split()[-1] _fontSize = int(int(fontSizeString.replace('pt', '')) / scale) _glyphs = self.fontGlyph[_fontSize] = AttrDict() _glyphs.fontName = fullFontNameString.replace(fontSizeString, '').strip() _glyphs.fontSize = _fontSize _glyphs._parent = self _glyphs.glyphs = [None] * 256 _glyphs.width = 0 _glyphs.height = 0 _glyphs.spaceWidth = 0 _glyphs.fontTransparency = fontTransparency # texture sizes dx = 1.0 / float(self.fontPNG.shape[1]) dy = 1.0 / float(self.fontPNG.shape[0]) for row in range(_startRow + 1, _kerningRow): line = self.fontInfo[row] lineVals = [int(ll) for ll in line.split()] if len(lineVals) == 9: chrNum, x, y, tx, ty, px, py, gw, gh = lineVals # only keep the simple chars for the minute if chrNum < 256: w = tx + LEFTBORDER + RIGHTBORDER h = ty + TOPBORDER + BOTTOMBORDER _kerns = [0] * 256 _glyphs.glyphs[chrNum] = GLGlyphTuple(x, y, tx, ty, px, py, gw, gh, _kerns, # coordinates in the texture x * dx, (y + h) * dy, (x + w) * dx, y * dy, # coordinates mapped to the quad px, gh - (py + h), px + (w), gh - py ) if chrNum == 65: # use 'A' for the referencing the tab size _glyphs.width = gw _glyphs.height = gh _glyphs.charWidth = gw _glyphs.charHeight = gh if chrNum == 32: # store the width of the space character _glyphs.spaceWidth = gw # fill the kerning lists for row in range(_kerningRow + 1, _nextRow): line = self.fontInfo[row] lineVals = [int(ll) for ll in line.split()] chrNum, chrNext, val = lineVals # set the kerning for valid values if (32 < chrNum < 256) and (32 < chrNext < 256): _glyphs.glyphs[chrNum].GlyphKerns[chrNext] = val
[docs] def get_kerning(self, fromChar, prevChar, glyphs): """Get the kerning required between the characters """ _glyph = glyphs[ord(fromChar)] if _glyph: return _glyph.GlyphKerns[ord(prevChar)] return 0
def __str__(self): """Information string for the font """ string = super().__str__() _fontSizes = [','.join(_glyph.fontSize for _glyph in self.fontGlyph.values())] string = '%s; name = %s; size = %s; file = %s' % (string, self.fontName, _fontSizes, self.fontFile) return string
[docs] def closestFont(self, size): """Get the closest font to the required size """ _size = min(list(self.fontGlyph.keys()), key=lambda x: abs(x - size)) return self.fontGlyph[_size]
[docs]class GLString(GLVertexArray): def __init__(self, text=None, font=None, obj=None, colour=(1.0, 1.0, 1.0, 1.0), x=0.0, y=0.0, ox=0.0, oy=0.0, angle=0.0, width=None, height=None, GLContext=None, blendMode=True, clearArrays=False, serial=None, pixelScale=None, alias=0): """ Create a GLString object for drawing text to the GL window :param text: :param font: :param obj: :param colour: :param x: :param y: :param ox: :param oy: :param angle: angle in degrees, negative is anti-clockwise :param width: :param height: :param GLContext: :param blendMode: :param clearArrays: :param serial: :param pixelScale: """ super().__init__(renderMode=GLRENDERMODE_DRAW, blendMode=blendMode, GLContext=GLContext, drawMode=GL.GL_TRIANGLES, dimension=2, clearArrays=clearArrays) if text is None: text = '' self.text = text self.font = font self.stringObject = obj # self.pid = obj.pid if hasattr(obj, 'pid') else None self.serial = serial self.colour = colour self._position = (x, y) self._offset = (ox, oy) self._angle = (3.1415926535 / 180) * angle self._scale = pixelScale or GLContext.viewports.devicePixelRatio # add scale here to render from a larger font? # - may need different offsets # - also need to modify getSmallFont self._alias = alias self.buildString()
[docs] def buildString(self): """Build the string """ text = self.text font = self.font colour = self.colour x, y = self._position ox, oy = self._offset # self.pid = self.stringObject.pid if hasattr(self.stringObject, 'pid') else None _glyphs = font.glyphs self.height = font.height self.width = 0 _validText = [tt for tt in text if _glyphs[ord(tt)] and ord(tt) > 32] lenText = len(_validText) # allocate space for all the letters, bad are discarded, spaces/tabs are not stored self.indices = np.empty(lenText * 6, dtype=np.uint32) self.vertices = np.empty(lenText * 8, dtype=np.float32) self.texcoords = np.empty(lenText * 8, dtype=np.float32) # self.attribs = np.zeros((len(text) * 4, 2), dtype=np.float32) # self.offsets = np.zeros((len(text) * 4, 2), dtype=np.float32) self.indexOffset = 0 penX = 0 penY = 0 # offset the string from (0, 0) and use (x, y) in shader prev = None if self._angle != 0.0: cs, sn = math.cos(self._angle), math.sin(self._angle) # rotate = np.matrix([[cs, sn], [-sn, cs]]) i = 0 for charCode in text: c = ord(charCode) glyph = _glyphs[c] if not glyph: # discard characters that are undefined continue if (c > 32): # visible characters kerning = font._parent.get_kerning(charCode, prev, _glyphs) if (prev and ord(prev) > 32) else 0 x0 = penX + glyph.GlyphPX0 + kerning y0 = penY + glyph.GlyphPY0 x1 = penX + glyph.GlyphPX1 + kerning y1 = penY + glyph.GlyphPY1 u0 = glyph.GlyphTX0 v0 = glyph.GlyphTY0 u1 = glyph.GlyphTX1 v1 = glyph.GlyphTY1 i4 = i * 4 i6 = i * 6 i8 = i * 8 if self._angle == 0.0: # horizontal text self.vertices[i8:i8 + 8] = (x0, y0, x0, y1, x1, y1, x1, y0) # pixel coordinates in string else: # apply rotation to the text xbl, ybl = x0 * cs + y0 * sn, -x0 * sn + y0 * cs xtl, ytl = x0 * cs + y1 * sn, -x0 * sn + y1 * cs xtr, ytr = x1 * cs + y1 * sn, -x1 * sn + y1 * cs xbr, ybr = x1 * cs + y0 * sn, -x1 * sn + y0 * cs self.vertices[i8:i8 + 8] = (xbl, ybl, xtl, ytl, xtr, ytr, xbr, ybr) # pixel coordinates in string self.indices[i6:i6 + 6] = (i4, i4 + 1, i4 + 2, i4, i4 + 2, i4 + 3) self.texcoords[i8:i8 + 8] = (u0, v0, u0, v1, u1, v1, u1, v0) # # store the attribs and offsets # self.attribs[i * 4:i * 4 + 4] = attribs # self.offsets[i * 4:i * 4 + 4] = offsets penX += glyph.GlyphOrigW + kerning i += 1 elif (c == 32): # space penX += font.spaceWidth elif (c == 10): # newline penX = 0 penY = 0 # penY + font.height # for vt in self.vertices: # vt[1] = vt[1] + font.height # occasional strange - RuntimeWarning: invalid value encountered in add # self.vertices[:, 1] += font.height # move all characters up by font height, centred bottom-left self.vertices[1:i * 8:2] += font.height self.height += font.height elif (c == 9): # tab penX += 4 * font.spaceWidth self.width = max(self.width, penX) # penY = penY + glyph[GlyphHeight] prev = charCode if not (0.9999 < self._scale < 1.0001): # apply font scaling for hi-res displays self.vertices /= self._scale self.height /= self._scale self.width /= self._scale # set the offsets for the characters to the desired coordinates self.numVertices = len(self.vertices) // 2 self.attribs = np.array((x + ox, y + oy, self._alias) * self.numVertices, dtype=np.float32) self.offsets = np.array((x, y) * self.numVertices, dtype=np.float32) self.stringOffset = None # (ox, oy) # set the colour for the whole string self.colors = np.array(colour * self.numVertices, dtype=np.float32) # create VBOs from the arrays self.defineTextArrayVBO()
# total width of text - probably don't need # width = penX - glyph.advance[0] / 64.0 + glyph.size[0]
[docs] def drawTextArray(self): """Draw text array with textures MUST be called inside GL current context, i.e., after GL.makeCurrent or inside initializeGL, paintGL """ # self._GLContext.globalGL._shaderProgramTex.setTextureID(self.font._parent.activeTextureNum) super().drawTextArray()
[docs] def drawTextArrayVBO(self, enableClientState=False, disableClientState=False): """Draw text array with textures and VBO MUST be called inside GL current context, i.e., after GL.makeCurrent or inside initializeGL, paintGL """ # self._GLContext.globalGL._shaderProgramTex.setTextureID(self.font._parent.activeTextureNum) super().drawTextArrayVBO()
[docs] def setStringColour(self, col): self.colour = col self.colors = np.array(self.colour * self.numVertices, dtype=np.float32)
[docs] def setStringHexColour(self, hexColour, alpha=1.0): col = hexToRgbRatio(hexColour) self.colour = (*col, alpha) self.colors = np.array(self.colour * self.numVertices, dtype=np.float32)
[docs] def setStringOffset(self, attrib): for pp in range(0, self.numVertices): self.attribs[3 * pp:3 * pp + 2] = self.offsets[2 * pp:2 * pp + 2] + attrib