#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''Class of text stimuli to be displayed in a :class:`~psychopy.visual.Window`
'''
# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
import os
import glob
import warnings
# Ensure setting pyglet.options['debug_gl'] to False is done prior to any
# other calls to pyglet or pyglet submodules, otherwise it may not get picked
# up by the pyglet GL engine and have no effect.
# Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+
import pyglet
pyglet.options['debug_gl'] = False
import ctypes
GL = pyglet.gl
import psychopy # so we can get the __path__
from psychopy import logging
# tools must only be imported *after* event or MovieStim breaks on win32
# (JWP has no idea why!)
from psychopy.tools.monitorunittools import cm2pix, deg2pix, convertToPix
from psychopy.tools.attributetools import attributeSetter, setAttribute
from psychopy.visual.basevisual import (
BaseVisualStim, DraggingMixin, ForeColorMixin, ContainerMixin, WindowMixin
)
from psychopy.colors import Color
# for displaying right-to-left (possibly bidirectional) text correctly:
from bidi import algorithm as bidi_algorithm # sufficient for Hebrew
# extra step needed to reshape Arabic/Farsi characters depending on
# their neighbours:
try:
from arabic_reshaper import ArabicReshaper
haveArabic = True
except ImportError:
haveArabic = False
import numpy
try:
import pygame
havePygame = True
except Exception:
havePygame = False
defaultLetterHeight = {'cm': 1.0,
'deg': 1.0,
'degs': 1.0,
'degFlatPos': 1.0,
'degFlat': 1.0,
'norm': 0.1,
'height': 0.2,
'pix': 20,
'pixels': 20}
defaultWrapWidth = {'cm': 15.0,
'deg': 15.0,
'degs': 15.0,
'degFlatPos': 15.0,
'degFlat': 15.0,
'norm': 1,
'height': 1,
'pix': 500,
'pixels': 500}
[docs]class TextStim(BaseVisualStim, DraggingMixin, ForeColorMixin, ContainerMixin):
"""Class of text stimuli to be displayed in a
:class:`~psychopy.visual.Window`
"""
def __init__(self, win,
text="Hello World",
font="",
pos=(0.0, 0.0),
depth=0,
rgb=None,
color=(1.0, 1.0, 1.0),
colorSpace='rgb',
opacity=1.0,
contrast=1.0,
units="",
ori=0.0,
height=None,
antialias=True,
bold=False,
italic=False,
alignHoriz=None,
alignVert=None,
alignText='center',
anchorHoriz='center',
anchorVert='center',
fontFiles=(),
wrapWidth=None,
flipHoriz=False,
flipVert=False,
languageStyle='LTR',
draggable=False,
name=None,
autoLog=None,
autoDraw=False):
"""
**Performance OBS:** in general, TextStim is slower than many other
visual stimuli, i.e. it takes longer to change some attributes.
In general, it's the attributes that affect the shapes of the letters:
``text``, ``height``, ``font``, ``bold`` etc.
These make the next .draw() slower because that sets the text again.
You can make the draw() quick by calling re-setting the text
(``myTextStim.text = myTextStim.text``) when you've changed the
parameters.
In general, other attributes which merely affect the presentation of
unchanged shapes are as fast as usual. This includes ``pos``,
``opacity`` etc.
The following attribute can only be set at initialization (see
further down for a list of attributes which can be changed after
initialization):
**languageStyle**
Apply settings to correctly display content from some languages
that are written right-to-left. Currently there are three (case-
insensitive) values for this parameter:
- ``'LTR'`` is the default, for typical left-to-right, Latin-style
languages.
- ``'RTL'`` will correctly display text in right-to-left languages
such as Hebrew. By applying the bidirectional algorithm, it
allows mixing portions of left-to-right content (such as numbers
or Latin script) within the string.
- ``'Arabic'`` applies the bidirectional algorithm but additionally
will _reshape_ Arabic characters so they appear in the cursive,
linked form that depends on neighbouring characters, rather than
in their isolated form. May also be applied in other scripts,
such as Farsi or Urdu, that use Arabic-style alphabets.
:Parameters:
"""
# what local vars are defined (these are the init params) for use by
# __repr__
self._initParams = dir()
self._initParams.remove('self')
"""
October 2018:
In place to remove the deprecation warning for pyglet.font.Text.
Temporary fix until pyglet.text.Label use is identical to pyglet.font.Text.
"""
warnings.filterwarnings(message='.*text.Label*', action='ignore')
super(TextStim, self).__init__(
win, units=units, name=name, autoLog=False)
self.draggable = draggable
if win.blendMode=='add':
logging.warning("Pyglet text does not honor the Window setting "
"`blendMode='add'` so 'avg' will be used for the "
"text (but objects drawn after can be added)")
self._needUpdate = True
self._needVertexUpdate = True
# use shaders if available by default, this is a good thing
self.__dict__['antialias'] = antialias
self.__dict__['font'] = font
self.__dict__['bold'] = bold
self.__dict__['italic'] = italic
# NB just a placeholder - real value set below
self.__dict__['text'] = ''
self.__dict__['depth'] = depth
self.__dict__['ori'] = ori
self.__dict__['flipHoriz'] = flipHoriz
self.__dict__['flipVert'] = flipVert
self.__dict__['languageStyle'] = languageStyle
if languageStyle.lower() == 'arabic':
arabic_config = {'delete_harakat': False, # if present, retain any diacritics
'shift_harakat_position': True} # shift by 1 to be compatible with the bidi algorithm
self.__dict__['arabic_reshaper'] = ArabicReshaper(configuration = arabic_config)
self._pygletTextObj = None
self.pos = pos
# deprecated attributes
if alignVert:
self.__dict__['alignVert'] = alignVert
logging.warning("TextStim.alignVert is deprecated. Use the "
"anchorVert attribute instead")
# for compatibility, alignText was historically 'left'
anchorVert = alignHoriz
if alignHoriz:
self.__dict__['alignHoriz'] = alignHoriz
logging.warning("TextStim.alignHoriz is deprecated. Use alignText "
"and anchorHoriz attributes instead")
# for compatibility, alignText was historically 'left'
alignText, anchorHoriz = alignHoriz, alignHoriz
# alignment and anchors
self.alignText = alignText
self.anchorHoriz = anchorHoriz
self.anchorVert = anchorVert
# generate the texture and list holders
self._listID = GL.glGenLists(1)
# pygame text needs a surface to render to:
if not self.win.winType in ["pyglet", "glfw"]:
self._texID = GL.GLuint()
GL.glGenTextures(1, ctypes.byref(self._texID))
# Color stuff
self.colorSpace = colorSpace
self.color = color
if rgb != None:
logging.warning("Use of rgb arguments to stimuli are deprecated. Please "
"use color and colorSpace args instead")
self.color = Color(rgb, 'rgb')
self.__dict__['fontFiles'] = []
self.fontFiles = list(fontFiles) # calls attributeSetter
self.setHeight(height, log=False) # calls setFont() at some point
# calls attributeSetter without log
setAttribute(self, 'wrapWidth', wrapWidth, log=False)
self.opacity = opacity
self.contrast = contrast
# self.width and self._fontHeightPix get set with text and
# calcSizeRendered is called
self.setText(text, log=False)
self._needUpdate = True
self.autoDraw = autoDraw
# set autoLog now that params have been initialised
wantLog = autoLog is None and self.win.autoLog
self.__dict__['autoLog'] = autoLog or wantLog
if self.autoLog:
logging.exp("Created %s = %s" % (self.name, str(self)))
def __del__(self):
if GL: # because of pytest fail otherwise
try:
GL.glDeleteLists(self._listID, 1)
except (ImportError, ModuleNotFoundError, TypeError, GL.lib.GLException):
pass # if pyglet no longer exists
@attributeSetter
def height(self, height):
"""The height of the letters (Float/int or None = set default).
Height includes the entire box that surrounds the letters
in the font. The width of the letters is then defined by the font.
:ref:`Operations <attrib-operations>` supported."""
# height in pix (needs to be done after units which is done during
# _Base.__init__)
if height is None:
if self.units in defaultLetterHeight:
height = defaultLetterHeight[self.units]
else:
msg = ("TextStim does now know a default letter height "
"for units %s")
raise AttributeError(msg % repr(self.units))
self.__dict__['height'] = height
self._heightPix = convertToPix(pos=numpy.array([0, 0]),
vertices=numpy.array([0, self.height]),
units=self.units, win=self.win)[1]
# need to update the font to reflect the change
self.setFont(self.font, log=False)
return self.__dict__['height']
@property
def size(self):
self.size = (self.height*len(self.text), self.height)
return WindowMixin.size.fget(self)
@size.setter
def size(self, value):
WindowMixin.size.fset(self, value)
self.height = getattr(self._size, self.units)[1]
[docs] def setHeight(self, height, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message. """
setAttribute(self, 'height', height, log)
[docs] def setLetterHeight(self, height, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message. """
setAttribute(self, 'height', height, log)
@attributeSetter
def font(self, font):
"""String. Set the font to be used for text rendering. font should
be a string specifying the name of the font (in system resources).
"""
self.__dict__['font'] = None # until we find one
if self.win.winType in ["pyglet", "glfw"]:
self._font = pyglet.font.load(font, int(self._heightPix),
dpi=72, italic=self.italic,
bold=self.bold)
self.__dict__['font'] = font
else:
if font is None or len(font) == 0:
self.__dict__['font'] = pygame.font.get_default_font()
elif font in pygame.font.get_fonts():
self.__dict__['font'] = font
elif type(font) == str:
# try to find a xxx.ttf file for it
# check for possible matching filenames
fontFilenames = glob.glob(font + '*')
if len(fontFilenames) > 0:
for thisFont in fontFilenames:
if thisFont[-4:] in ['.TTF', '.ttf']:
# take the first match
self.__dict__['font'] = thisFont
break # stop at the first one we find
# trhen check if we were successful
if self.font is None and font != "":
# we didn't find a ttf filename
msg = ("Found %s but it doesn't end .ttf. "
"Using default font.")
logging.warning(msg % fontFilenames[0])
self.__dict__['font'] = pygame.font.get_default_font()
if self.font is not None and os.path.isfile(self.font):
self._font = pygame.font.Font(self.font, int(
self._heightPix), italic=self.italic, bold=self.bold)
else:
try:
self._font = pygame.font.SysFont(
self.font, int(self._heightPix), italic=self.italic,
bold=self.bold)
self.__dict__['font'] = font
logging.info('using sysFont ' + str(font))
except Exception:
self.__dict__['font'] = pygame.font.get_default_font()
msg = ("Couldn't find font %s on the system. Using %s "
"instead! Font names should be written as "
"concatenated names all in lower case.\ne.g. "
"'arial', 'monotypecorsiva', 'rockwellextra', ...")
logging.error(msg % (font, self.font))
self._font = pygame.font.SysFont(
self.font, int(self._heightPix), italic=self.italic,
bold=self.bold)
# re-render text after a font change
self._needSetText = True
[docs] def setFont(self, font, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'font', font, log)
@attributeSetter
def text(self, text):
"""The text to be rendered. Use \\\\n to make new lines.
Issues: May be slow, and pyglet has a memory leak when setting text.
For these reasons, this function checks so that it only updates the
text if it has changed. So scripts can safely set the text on every
frame, with no need to check if it has actually altered.
"""
if text == self.text: # only update for a change
return
if text is not None:
text = str(text) # make sure we have unicode object to render
# deal with some international text issues. Only relevant for Python:
# online experiments use web technologies and handle this seamlessly.
style = self.languageStyle.lower() # be flexible with case
if style == 'arabic' and haveArabic:
# reshape Arabic characters from their isolated form so that
# they flow and join correctly to their neighbours:
text = self.arabic_reshaper.reshape(text)
if style == 'rtl' or (style == 'arabic' and haveArabic):
# deal with right-to-left text presentation by applying the
# bidirectional algorithm:
text = bidi_algorithm.get_display(text)
# no action needed for default 'ltr' (left-to-right) option
self.__dict__['text'] = text
self._setTextShaders(text)
self._needSetText = False
return self.__dict__['text']
[docs] def setText(self, text=None, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'text', text, log)
[docs] def _setTextShaders(self, value=None):
"""Set the text to be rendered using the current font
"""
if self.win.winType in ["pyglet", "glfw"]:
rgba255 = self._foreColor.rgba255
rgba255[3] = rgba255[3]*255
rgba255 = [int(c) for c in rgba255]
self._pygletTextObj = pyglet.text.Label(
self.text, self.font, int(self._heightPix*0.75),
italic=self.italic,
bold=self.bold,
anchor_x=self.anchorHoriz,
anchor_y=self.anchorVert, # the point we rotate around
align=self.alignText,
color=rgba255,
multiline=True, width=self._wrapWidthPix) # width of the frame
self.width = self._pygletTextObj.width
self._fontHeightPix = self._pygletTextObj.height
else:
self._surf = self._font.render(value, self.antialias,
[255, 255, 255])
self.width, self._fontHeightPix = self._surf.get_size()
if self.antialias:
smoothing = GL.GL_LINEAR
else:
smoothing = GL.GL_NEAREST
# generate the textures from pygame surface
GL.glEnable(GL.GL_TEXTURE_2D)
# bind that name to the target
GL.glBindTexture(GL.GL_TEXTURE_2D, self._texID)
GL.gluBuild2DMipmaps(GL.GL_TEXTURE_2D, 4, self.width,
self._fontHeightPix,
GL.GL_RGBA, GL.GL_UNSIGNED_BYTE,
pygame.image.tostring(self._surf, "RGBA", 1))
# linear smoothing if texture is stretched?
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER,
smoothing)
# but nearest pixel value if it's compressed?
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER,
smoothing)
self._needSetText = False
self._needUpdate = True
[docs] def _updateListShaders(self):
"""Only used with pygame text - pyglet handles all from the draw()
"""
if self._needSetText:
self.setText(log=False)
GL.glNewList(self._listID, GL.GL_COMPILE)
# GL.glPushMatrix()
# setup the shaderprogram
# no need to do texture maths so no need for programs?
# If we're using pyglet then this list won't be called, and for pygame
# shaders aren't enabled
GL.glUseProgram(0) # self.win._progSignedTex)
# GL.glUniform1i(GL.glGetUniformLocation(self.win._progSignedTex,
# "texture"), 0) # set the texture to be texture unit 0
# coords:
if self.alignHoriz in ['center', 'centre']:
left = -self.width/2.0
right = self.width/2.0
elif self.alignHoriz == 'right':
left = -self.width
right = 0.0
else:
left = 0.0
right = self.width
# how much to move bottom
if self.alignVert in ['center', 'centre']:
bottom = -self._fontHeightPix/2.0
top = self._fontHeightPix/2.0
elif self.alignVert == 'top':
bottom = -self._fontHeightPix
top = 0
else:
bottom = 0.0
top = self._fontHeightPix
# there seems to be a rounding err in pygame font textures
Btex, Ttex, Ltex, Rtex = -0.01, 0.98, 0, 1.0
# unbind the mask texture regardless
GL.glActiveTexture(GL.GL_TEXTURE1)
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
if self.win.winType in ["pyglet", "glfw"]:
# unbind the main texture
GL.glActiveTexture(GL.GL_TEXTURE0)
# GL.glActiveTextureARB(GL.GL_TEXTURE0_ARB)
# the texture is specified by pyglet.font.GlyphString.draw()
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
GL.glEnable(GL.GL_TEXTURE_2D)
else:
# bind the appropriate main texture
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glBindTexture(GL.GL_TEXTURE_2D, self._texID)
GL.glEnable(GL.GL_TEXTURE_2D)
if self.win.winType in ["pyglet", "glfw"]:
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glEnable(GL.GL_TEXTURE_2D)
self._pygletTextObj.draw()
else:
# draw a 4 sided polygon
GL.glBegin(GL.GL_QUADS)
# right bottom
GL.glMultiTexCoord2f(GL.GL_TEXTURE0, Rtex, Btex)
GL.glVertex3f(right, bottom, 0)
# left bottom
GL.glMultiTexCoord2f(GL.GL_TEXTURE0, Ltex, Btex)
GL.glVertex3f(left, bottom, 0)
# left top
GL.glMultiTexCoord2f(GL.GL_TEXTURE0, Ltex, Ttex)
GL.glVertex3f(left, top, 0)
# right top
GL.glMultiTexCoord2f(GL.GL_TEXTURE0, Rtex, Ttex)
GL.glVertex3f(right, top, 0)
GL.glEnd()
GL.glDisable(GL.GL_TEXTURE_2D)
GL.glUseProgram(0)
# GL.glPopMatrix()
GL.glEndList()
self._needUpdate = False
@attributeSetter
def flipHoriz(self, value):
"""If set to True then the text will be flipped left-to-right. The
flip is relative to the original, not relative to the current state.
"""
self.__dict__['flipHoriz'] = value
[docs] def setFlipHoriz(self, newVal=True, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message.
"""
setAttribute(self, 'flipHoriz', newVal, log)
@attributeSetter
def flipVert(self, value):
"""If set to True then the text will be flipped top-to-bottom. The
flip is relative to the original, not relative to the current state.
"""
self.__dict__['flipVert'] = value
[docs] def setFlipVert(self, newVal=True, log=None):
"""Usually you can use 'stim.attribute = value' syntax instead,
but use this method if you need to suppress the log message
"""
setAttribute(self, 'flipVert', newVal, log)
[docs] def setFlip(self, direction, log=None):
"""(used by Builder to simplify the dialog)
"""
if direction == 'vert':
self.setFlipVert(True, log=log)
elif direction == 'horiz':
self.setFlipHoriz(True, log=log)
@attributeSetter
def antialias(self, value):
"""Allow antialiasing the text (True or False). Sets text, slow.
"""
self.__dict__['antialias'] = value
self._needSetText = True
@attributeSetter
def bold(self, value):
"""Make the text bold (True, False) (better to use a bold font name).
"""
self.__dict__['bold'] = value
self.font = self.font # call attributeSetter
@attributeSetter
def italic(self, value):
"""True/False.
Make the text italic (better to use a italic font name).
"""
self.__dict__['italic'] = value
self.font = self.font # call attributeSetter
@attributeSetter
def alignHoriz(self, value):
"""Deprecated in PsychoPy 3.3. Use `alignText` and `anchorHoriz`
instead
"""
self.__dict__['alignHoriz'] = value
self._needSetText = True
@attributeSetter
def alignVert(self, value):
"""Deprecated in PsychoPy 3.3. Use `anchorVert`
"""
self.__dict__['alignVert'] = value
self._needSetText = True
@attributeSetter
def alignText(self, value):
"""Aligns the text content within the bounding box ('left', 'right' or
'center')
See also `anchorX` to set alignment of the box itself relative to pos
"""
self.__dict__['alignText'] = value
self._needSetText = True
@attributeSetter
def anchorHoriz(self, value):
"""The horizontal alignment ('left', 'right' or 'center')
"""
self.__dict__['anchorHoriz'] = value
self._needSetText = True
@attributeSetter
def anchorVert(self, value):
"""The vertical alignment ('top', 'bottom' or 'center') of the box
relative to the text `pos`.
"""
self.__dict__['anchorVert'] = value
self._needSetText = True
@attributeSetter
def fontFiles(self, fontFiles):
"""A list of additional files if the font is not in the standard
system location (include the full path).
OBS: fonts are added every time this value is set. Previous are
not deleted.
E.g.::
stim.fontFiles = ['SpringRage.ttf'] # load file(s)
stim.font = 'SpringRage' # set to font
"""
self.__dict__['fontFiles'] += fontFiles
for thisFont in fontFiles:
pyglet.font.add_file(thisFont)
@attributeSetter
def wrapWidth(self, wrapWidth):
"""Int/float or None (set default).
The width the text should run before wrapping.
:ref:`Operations <attrib-operations>` supported.
"""
if wrapWidth is None:
if self.units in defaultWrapWidth:
wrapWidth = defaultWrapWidth[self.units]
else:
msg = "TextStim does now know a default wrap width for units %s"
raise AttributeError(msg % repr(self.units))
self.__dict__['wrapWidth'] = wrapWidth
verts = numpy.array([self.wrapWidth, 0])
self._wrapWidthPix = convertToPix(pos=numpy.array([0, 0]),
vertices=verts,
units=self.units, win=self.win)[0]
self._needSetText = True
@property
def boundingBox(self):
"""(read only) attribute representing the bounding box of the text
(w,h). This differs from `width` in that the width represents the
width of the margins, which might differ from the width of the text
within them.
NOTE: currently always returns the size in pixels
(this will change to return in stimulus units)
"""
if hasattr(self._pygletTextObj, 'content_width'):
w, h = (self._pygletTextObj.content_width,
self._pygletTextObj.content_height)
else:
w, h = (self._pygletTextObj._layout.content_width,
self._pygletTextObj._layout.content_height)
return w, h
@property
def posPix(self):
"""This determines the coordinates in pixels of the position for the
current stimulus, accounting for pos and units. This property should
automatically update if `pos` is changed"""
# because this is a property getter we can check /on-access/ if it
# needs updating :-)
if self._needVertexUpdate:
self.__dict__['posPix'] = self._pos.pix
self._needVertexUpdate = False
return self.__dict__['posPix']
[docs] def updateOpacity(self):
self._setTextShaders(value=self.text)
[docs] def draw(self, win=None):
"""
Draw the stimulus in its relevant window. You must call
this method after every MyWin.flip() if you want the
stimulus to appear on that frame and then update the screen
again.
If win is specified then override the normal window of this stimulus.
"""
if win is None:
win = self.win
self._selectWindow(win)
blendMode = win.blendMode # keep track for reset later
GL.glPushMatrix()
# for PyOpenGL this is necessary despite pop/PushMatrix, (not for
# pyglet)
GL.glLoadIdentity()
#scale and rotate
prevScale = win.setScale('pix') # to units for translations
# NB depth is set already
GL.glTranslatef(self.posPix[0], self.posPix[1], 0)
GL.glRotatef(-self.ori, 0.0, 0.0, 1.0)
# back to pixels for drawing surface
win.setScale('pix', None, prevScale)
GL.glScalef((1, -1)[self.flipHoriz], (1, -1)
[self.flipVert], 1) # x,y,z; -1=flipped
# setup color
GL.glColor4f(*self._foreColor.render('rgba1'))
GL.glUseProgram(self.win._progSignedTexFont)
# GL.glUniform3iv(GL.glGetUniformLocation(
# self.win._progSignedTexFont, "rgb"), 1,
# desiredRGB.ctypes.data_as(ctypes.POINTER(ctypes.c_float)))
# # set the texture to be texture unit 0
GL.glUniform3f(
GL.glGetUniformLocation(self.win._progSignedTexFont, b"rgb"),
*self._foreColor.render('rgb1'))
# should text have a depth or just on top?
GL.glDisable(GL.GL_DEPTH_TEST)
# update list if necss and then call it
if win.winType in ["pyglet", "glfw"]:
if self._needSetText:
self.setText()
# unbind the mask texture regardless
GL.glActiveTexture(GL.GL_TEXTURE1)
GL.glEnable(GL.GL_TEXTURE_2D)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
# unbind the main texture
GL.glActiveTexture(GL.GL_TEXTURE0)
GL.glEnable(GL.GL_TEXTURE_2D)
# then allow pyglet to bind and use texture during drawing
self._pygletTextObj.draw()
GL.glDisable(GL.GL_TEXTURE_2D)
else:
# for pygame we should (and can) use a drawing list
if self._needUpdate:
self._updateList()
GL.glCallList(self._listID)
# pyglets text.draw() method alters the blend func so reassert ours
win.setBlendMode(blendMode, log=False)
GL.glUseProgram(0)
# GL.glEnable(GL.GL_DEPTH_TEST) # Enables Depth Testing
GL.glPopMatrix()