2
# Major library imports
5
from numpy import array, ndarray
7
# Pyglet and pyglet-related imports
8
# Before we import anything else from pyglet, we need to set the shadow_window
9
# option to False, so that it does not conflict with WX, in case someone is
10
# trying to use the kiva GL GraphicsContext from within WX.
11
# This is necessary as of pyglet 1.1.
13
pyglet.options['shadow_window'] = False
15
#from pyglet.font import Text
16
from pyglet.text import Label
17
from pyglet.font import load as load_font
18
from pyglet.font.base import Font as PygletFont
19
from pyglet.graphics import Batch
21
from pygarrayimage.arrayimage import ArrayInterfaceImage
24
from affine import affine_from_values, transform_points
25
from agg import GraphicsContextGL as _GCL
26
from agg import GraphicsContextArray
27
from agg import AggFontType
29
from agg import CompiledPath
30
from constants import BOLD, BOLD_ITALIC, ITALIC
31
from fonttools import Font
34
class ArrayImage(ArrayInterfaceImage):
35
""" pyglet ImageData made from numpy arrays.
37
Customized from pygarrayimage's ArrayInterfaceImage to override the texture
41
def create_texture(self, cls):
42
'''Create a texture containing this image.
44
If the image's dimensions are not powers of 2, a TextureRegion of
45
a larger Texture will be returned that matches the dimensions of this
49
`cls` : class (subclass of Texture)
52
:rtype: cls or cls.region_class
55
texture = cls.create_for_size(
56
gl.GL_TEXTURE_2D, self.width, self.height)
58
if texture.width != self.width or texture.height != self.height:
59
texture = texture.get_region(0, 0, self.width, self.height)
62
internalformat = self._get_internalformat(self.format)
64
gl.glBindTexture(texture.target, texture.id)
65
gl.glTexParameteri(texture.target, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
66
gl.glTexParameteri(texture.target, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
67
gl.glTexParameteri(texture.target, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
68
gl.glTexParameteri(texture.target, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
71
width = texture.owner.width
72
height = texture.owner.height
73
blank = (ctypes.c_ubyte * (width * height * 4))()
74
gl.glTexImage2D(texture.target, texture.level,
78
gl.GL_RGBA, gl.GL_UNSIGNED_BYTE,
82
self.blit_to_texture(texture.target, texture.level,
83
0, 0, 0, internalformat)
87
def blit_to_texture(self, target, level, x, y, z, internalformat=None):
88
'''Draw this image to to the currently bound texture at `target`.
90
If `internalformat` is specified, glTexImage is used to initialise
91
the texture; otherwise, glTexSubImage is used to update a region.
94
data_format = self.format
95
data_pitch = abs(self._current_pitch)
97
# Determine pixel format from format string
99
format, type = self._get_gl_format_and_type(data_format)
101
if (len(data_format) in (3, 4) and
102
gl.gl_info.have_extension('GL_ARB_imaging')):
103
# Construct a color matrix to convert to GL_RGBA
104
def component_column(component):
106
pos = 'RGBA'.index(component)
107
return [0] * pos + [1] + [0] * (3 - pos)
110
# pad to avoid index exceptions
111
lookup_format = data_format + 'XXX'
112
matrix = (component_column(lookup_format[0]) +
113
component_column(lookup_format[1]) +
114
component_column(lookup_format[2]) +
115
component_column(lookup_format[3]))
118
4: gl.GL_RGBA}.get(len(data_format))
119
type = gl.GL_UNSIGNED_BYTE
121
gl.glMatrixMode(gl.GL_COLOR)
123
gl.glLoadMatrixf((gl.GLfloat * 16)(*matrix))
125
# Need to convert data to a standard form
130
4: 'RGBA'}.get(len(data_format))
131
format, type = self._get_gl_format_and_type(data_format)
133
# Workaround: don't use GL_UNPACK_ROW_LENGTH
134
if gl._current_context._workaround_unpack_row_length:
135
data_pitch = self.width * len(data_format)
137
# Get data in required format (hopefully will be the same format it's
138
# already in, unless that's an obscure format, upside-down or the
140
data = self._convert(data_format, data_pitch)
144
elif data_pitch & 0x2:
148
row_length = data_pitch / len(data_format)
149
gl.glPushClientAttrib(gl.GL_CLIENT_PIXEL_STORE_BIT)
150
gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
151
gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, row_length)
152
self._apply_region_unpack()
153
gl.glTexParameteri(target, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
154
gl.glTexParameteri(target, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
155
gl.glTexParameteri(target, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
156
gl.glTexParameteri(target, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
159
if target == gl.GL_TEXTURE_3D:
160
assert not internalformat
161
gl.glTexSubImage3D(target, level,
163
self.width, self.height, 1,
167
gl.glTexImage2D(target, level,
169
self.width, self.height,
174
gl.glTexSubImage2D(target, level,
176
self.width, self.height,
179
gl.glPopClientAttrib()
183
gl.glMatrixMode(gl.GL_MODELVIEW)
186
def image_as_array(img):
187
""" Adapt an image object into a numpy array.
189
Typically, this is used to adapt an agg GraphicsContextArray which has been
190
used for image storage in Kiva applications.
192
if hasattr(img, 'bmp_array'):
193
# Yup, a GraphicsContextArray.
195
elif isinstance(img, ndarray):
198
raise NotImplementedError("can't convert %r into a numpy array" % (img,))
201
""" Returns the appropriate DPI setting for the system"""
205
def __init__(self, *args, **kw):
206
# An ordering of keys in self; the last item was the most recently used
208
self.__maxlength__ = 30
209
dict.__init__(self, *args, **kw)
211
def __getitem__(self, key):
212
val = dict.__getitem__(self, key)
213
# If we get here, then key was found in our dict
217
def __setitem__(self, key, value):
218
dict.__setitem__(self, key, value)
221
def __delitem__(self, key):
222
dict.__delitem__(self, key)
223
if key in self.__order__:
224
self.__order__.remove(key)
226
def __touch__(self, key):
227
""" Puts **key** as the most recently used item """
228
if len(self.__order__) == 0:
229
self.__order__.append(key)
230
if (len(self.__order__) == self.__maxlength__) and key not in self.__order__:
231
# The MRU is full, so pop the oldest element
232
del self[self.__order__[0]]
233
if key != self.__order__[-1]:
235
ndx = self.__order__.index(key)
236
self.__order__[ndx:-1] = self.__order__[ndx+1:]
237
self.__order__[-1] = key
239
# new key that's not already in the cache
240
if len(self.__order__) == self.__maxlength__:
241
self.__order__ = self.__order__[1:] + [key]
243
self.__order__.append(key)
246
# Use a singleton for the font cache
247
GlobalFontCache = MRU()
249
""" Returns a Pylget Font object for the given Agg or Kiva font """
250
if isinstance(font, PygletFont):
254
key = (font.name, font.size, font.family, font.style)
255
if key not in GlobalFontCache:
256
if isinstance(font, AggFontType):
258
font = Font(face_name = agg_font.name,
259
size = agg_font.size,
260
family = agg_font.family,
261
style = agg_font.style)
264
if font.style == BOLD or font.style == BOLD_ITALIC or font.weight == BOLD:
266
if font.style == ITALIC or font.style == BOLD_ITALIC:
268
pyglet_font = load_font(font.findfontname(), font.size, bold, italic)
269
GlobalFontCache[key] = pyglet_font
271
pyglet_font = GlobalFontCache[key]
275
# Because Pyglet 1.1 uses persistent Label objects to efficiently lay
276
# out and render text, we cache these globally to minimize the creation
277
# time. An MRU is not really the right structure to use here, though.
278
# (We typically expect that the same numbers of labels will be rendered.)
279
GlobalTextCache = MRU()
280
GlobalTextCache.__maxlength__ = 100
281
def GetLabel(text, pyglet_font):
282
""" Returns a Pyglet Label object for the given text and font """
283
key = (text, pyglet_font)
284
if key not in GlobalTextCache:
285
# Use anchor_y="bottom" because by default, pyglet sets the baseline to
286
# the y coordinate given. Unfortunately, it doesn't expose a per-Text
287
# descent (only a per-Font descent), so it's impossible to know how to
288
# offset the y value properly for a given string.
289
label = Label(text, font_name=pyglet_font.name, font_size=pyglet_font.size,
291
GlobalTextCache[key] = label
293
label = GlobalTextCache[key]
297
class GraphicsContext(_GCL):
298
def __init__(self, size, *args, **kw):
299
# Ignore the pix_format argument for now
300
if "pix_format" in kw:
302
_GCL.__init__(self, size[0], size[1], *args, **kw)
303
self.corner_pixel_origin = True
305
self._font_stack = []
306
self._current_font = None
308
def save_state(self):
309
super(GraphicsContext, self).save_state()
310
self._font_stack.append(self._current_font)
312
def restore_state(self):
313
super(GraphicsContext, self).restore_state()
314
self._current_font = self._font_stack.pop()
316
def set_font(self, font):
317
super(GraphicsContext, self).set_font(font)
318
self._current_font = font
320
def get_text_extent(self, text):
321
if self._current_font is None:
324
pyglet_font = GetFont(self._current_font)
325
label = GetLabel(text, pyglet_font)
326
return (0, 0, label.content_width, label.content_height)
328
def show_text(self, text, point = None):
331
return self.show_text_at_point(text, *point)
333
def show_text_at_point(self, text, x, y):
334
if self._current_font is None:
337
pyglet_font = GetFont(self._current_font)
338
label = GetLabel(text, pyglet_font)
340
xform = self.get_ctm()
344
# The GL backend places the center of a pixel at (0.5, 0.5); however,
345
# for show_text_at_point, we don't actually want to render the text
346
# offset by half a pixel. There is probably a better, more uniform way
347
# to handle this across all of Kiva, because this is probably a common
348
# issue that will arise, but for now, we just round the position down.
354
c = self.get_fill_color()
355
label.color = (int(c[0]*255), int(c[1]*255), int(c[2]*255), int(c[3]*255))
359
def draw_image(self, img, rect=None, force_copy=False):
360
""" Renders a GraphicsContextArray into this GC """
361
xform = self.get_ctm()
365
image = image_as_array(img)
371
aii = ArrayImage(image, format=fmt)
372
texture = aii.texture
374
# The texture coords consists of (u,v,r) for each corner of the
375
# texture rectangle. The coordinates are stored in the order
376
# bottom left, bottom right, top right, top left.
380
t = texture.tex_coords
387
p = transform_points(affine_from_values(*xform), points)
389
t[0], t[1], t[2], 1.,
390
p[0,0], p[0,1], 0, 1.,
391
t[3], t[4], t[5], 1.,
392
p[1,0], p[1,1], 0, 1.,
393
t[6], t[7], t[8], 1.,
394
p[2,0], p[2,1], 0, 1.,
395
t[9], t[10], t[11], 1.,
396
p[3,0], p[3,1], 0, 1.,
398
gl.glPushAttrib(gl.GL_ENABLE_BIT)
399
gl.glEnable(texture.target)
400
gl.glBindTexture(texture.target, texture.id)
401
gl.glPushClientAttrib(gl.GL_CLIENT_VERTEX_ARRAY_BIT)
402
gl.glInterleavedArrays(gl.GL_T4F_V4F, 0, a)
403
gl.glDrawArrays(gl.GL_QUADS, 0, 4)
404
gl.glPopClientAttrib()
407
def font_metrics_provider():
408
return GraphicsContext((1,1))