Repository URL to install this package:
Version:
5.3.0 ▾
|
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
import ctypes
import logging
from math import floor
import sys
from numpy import array, ndarray
# Local kiva imports
from kiva.affine import affine_from_values, transform_points
from kiva.constants import BOLD, bold_styles, italic_styles
from kiva.fonttools import Font
from kiva.gl.gl import CompiledPath, GraphicsContextGL, KivaGLFontType
logger = logging.getLogger(__name__)
def image_as_array(img):
""" Adapt an image object into a numpy array.
Typically, this is used to adapt an agg GraphicsContextArray which has been
used for image storage in Kiva applications.
"""
from PIL import Image
if hasattr(img, "bmp_array"):
# Yup, a GraphicsContextArray.
img = Image.fromarray(img.bmp_array)
elif isinstance(img, ndarray):
img = Image.fromarray(img)
elif isinstance(img, Image.Image):
pass
else:
msg = "can't convert %r into a numpy array" % (img,)
raise NotImplementedError(msg)
# Ensure RGB or RGBA formats
if not img.mode.startswith("RGB"):
img = img.convert("RGB")
return array(img)
def get_dpi():
""" Returns the appropriate DPI setting for the system"""
pass
class MRU(dict):
def __init__(self, *args, **kw):
# An ordering of keys in self; the last item was the most recently used
self.__order__ = []
self.__maxlength__ = 30
dict.__init__(self, *args, **kw)
def __getitem__(self, key):
val = dict.__getitem__(self, key)
# If we get here, then key was found in our dict
self.__touch__(key)
return val
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self.__touch__(key)
def __delitem__(self, key):
dict.__delitem__(self, key)
if key in self.__order__:
self.__order__.remove(key)
def __touch__(self, key):
""" Puts **key** as the most recently used item """
if len(self.__order__) == 0:
self.__order__.append(key)
if (len(self.__order__) == self.__maxlength__ and
key not in self.__order__):
# The MRU is full, so pop the oldest element
del self[self.__order__[0]]
if key != self.__order__[-1]:
try:
ndx = self.__order__.index(key)
self.__order__[ndx:-1] = self.__order__[ndx + 1:]
self.__order__[-1] = key
except ValueError:
# new key that's not already in the cache
if len(self.__order__) == self.__maxlength__:
self.__order__ = self.__order__[1:] + [key]
else:
self.__order__.append(key)
# Pyglet and pyglet-related imports
# Before we import anything else from pyglet, we need to set the shadow_window
# option to False, so that it does not conflict with WX, in case someone is
# trying to use the kiva GL GraphicsContext from within WX.
# This is necessary as of pyglet 1.1.
try:
import pyglet
pyglet.options["shadow_window"] = False
from pyglet.text import Label
from pyglet.font import load as load_font
from pyglet.font.base import Font as PygletFont
from pyglet import gl
from pygarrayimage.arrayimage import ArrayInterfaceImage
class _ObjectSpace(object):
""" Object space mocker
Source: https://github.com/ColinDuquesnoy/QPygletWidget
"""
def __init__(self):
# Textures and buffers scheduled for deletion the next time this
# object space is active.
self._doomed_textures = []
self._doomed_buffers = []
class FakePygletContext(object):
""" pyglet.gl.Context mocker.
This is used to make pyglet believe that a valid context has already
been setup. (Qt takes care of creating the open gl context)
_Most of the methods are empty, there is just the minimum required to
make it look like a duck..._
Source: https://github.com/ColinDuquesnoy/QPygletWidget
"""
# define the same class attribute as pyglet.gl.Context
CONTEXT_SHARE_NONE = None
CONTEXT_SHARE_EXISTING = 1
_gl_begin = False
_info = None
_workaround_checks = [
(
"_workaround_unpack_row_length",
lambda info: info.get_renderer() == "GDI Generic",
),
(
"_workaround_vbo",
lambda info: info.get_renderer().startswith("ATI Radeon X"),
),
(
"_workaround_vbo_finish",
lambda info: (
"ATI" in info.get_renderer()
and info.have_version(1, 5)
and sys.platform == "darwin"
),
),
]
def __init__(self, context_share=None):
""" Setup workaround attr and object spaces
(again to mock what is done in pyglet context)
"""
self.object_space = _ObjectSpace()
for attr, check in self._workaround_checks:
setattr(self, attr, None)
def __repr__(self):
return "%s()" % self.__class__.__name__
def set_current(self):
gl.current_context = self
def destroy(self):
pass
def delete_texture(self, texture_id):
pass
def delete_buffer(self, buffer_id):
pass
class ArrayImage(ArrayInterfaceImage):
""" pyglet ImageData made from numpy arrays.
Customized from pygarrayimage's ArrayInterfaceImage to override the
texture creation.
"""
def create_texture(self, cls, rectangle=False, force_rectangle=False):
""" Create a texture containing this image.
If the image's dimensions are not powers of 2, a TextureRegion of
a larger Texture will be returned that matches the dimensions of
this image.
:Parameters:
`cls` : class (subclass of Texture)
Class to construct.
`rectangle` : bool
``True`` if a rectangle can be created; see
`AbstractImage.get_texture`.
:rtype: cls or cls.region_class
"""
_is_pow2 = (lambda v: (v & (v - 1)) == 0)
target = gl.GL_TEXTURE_2D
if (rectangle
and not (_is_pow2(self.width) and _is_pow2(self.height))):
if gl.gl_info.have_extension("GL_ARB_texture_rectangle"):
target = gl.GL_TEXTURE_RECTANGLE_ARB
elif gl.gl_info.have_extension("GL_NV_texture_rectangle"):
target = gl.GL_TEXTURE_RECTANGLE_NV
texture = cls.create_for_size(target, self.width, self.height)
subimage = False
if texture.width != self.width or texture.height != self.height:
texture = texture.get_region(0, 0, self.width, self.height)
subimage = True
internalformat = self._get_internalformat(self.format)
gl.glBindTexture(texture.target, texture.id)
gl.glTexParameteri(
texture.target, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE
)
gl.glTexParameteri(
texture.target, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE
)
gl.glTexParameteri(
texture.target, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR
)
gl.glTexParameteri(
texture.target, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR
)
if subimage:
width = texture.owner.width
height = texture.owner.height
blank = (ctypes.c_ubyte * (width * height * 4))()
gl.glTexImage2D(
texture.target,
texture.level,
internalformat,
width,
height,
1,
gl.GL_RGBA,
gl.GL_UNSIGNED_BYTE,
blank,
)
internalformat = None
self.blit_to_texture(
texture.target, texture.level, 0, 0, 0, internalformat
)
return texture
def blit_to_texture(self, target, level, x, y, z, internalformat=None):
"""Draw this image to to the currently bound texture at `target`.
If `internalformat` is specified, glTexImage is used to initialise
the texture; otherwise, glTexSubImage is used to update a region.
"""
data_format = self.format
data_pitch = abs(self._current_pitch)
# Determine pixel format from format string
matrix = None
format, type = self._get_gl_format_and_type(data_format)
if format is None:
if (len(data_format) in (3, 4)
and gl.gl_info.have_extension("GL_ARB_imaging")):
# Construct a color matrix to convert to GL_RGBA
def component_column(component):
try:
pos = "RGBA".index(component)
return [0] * pos + [1] + [0] * (3 - pos)
except ValueError:
return [0, 0, 0, 0]
# pad to avoid index exceptions
lookup_format = data_format + "XXX"
matrix = (
component_column(lookup_format[0])
+ component_column(lookup_format[1])
+ component_column(lookup_format[2])
+ component_column(lookup_format[3])
)
format = {3: gl.GL_RGB, 4: gl.GL_RGBA}.get(
len(data_format)
)
type = gl.GL_UNSIGNED_BYTE
gl.glMatrixMode(gl.GL_COLOR)
gl.glPushMatrix()
gl.glLoadMatrixf((gl.GLfloat * 16)(*matrix))
else:
# Need to convert data to a standard form
data_format = {1: "L", 2: "LA", 3: "RGB", 4: "RGBA"}.get(
len(data_format)
)
format, type = self._get_gl_format_and_type(data_format)
# Workaround: don't use GL_UNPACK_ROW_LENGTH
if gl.current_context._workaround_unpack_row_length:
data_pitch = self.width * len(data_format)
# Get data in required format (hopefully will be the same format
# it's already in, unless that's an obscure format, upside-down or
# the driver is old).
data = self._convert(data_format, data_pitch)
if data_pitch & 0x1:
alignment = 1
elif data_pitch & 0x2:
alignment = 2
else:
alignment = 4
row_length = data_pitch / len(data_format)
gl.glPushClientAttrib(gl.GL_CLIENT_PIXEL_STORE_BIT)
gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, alignment)
gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, row_length)
self._apply_region_unpack()
gl.glTexParameteri(
target, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE
)
gl.glTexParameteri(
target, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE
)
gl.glTexParameteri(target, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
gl.glTexParameteri(target, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
if target == gl.GL_TEXTURE_3D:
assert not internalformat
gl.glTexSubImage3D(
target,
level,
x,
y,
z,
self.width,
self.height,
1,
format,
type,
data,
)
elif internalformat:
gl.glTexImage2D(
target,
level,
internalformat,
self.width,
self.height,
0,
format,
type,
data,
)
else:
gl.glTexSubImage2D(
target,
level,
x,
y,
self.width,
self.height,
format,
type,
data,
)
gl.glPopClientAttrib()
if matrix:
gl.glPopMatrix()
gl.glMatrixMode(gl.GL_MODELVIEW)
# Use a singleton for the font cache
GlobalFontCache = MRU()
def GetFont(font):
""" Returns a Pylget Font object for the given Agg or Kiva font """
if isinstance(font, PygletFont):
pyglet_font = font
else:
# KivaGLFontType
key = (font.name, font.size, font.family, font.style)
if key not in GlobalFontCache:
if isinstance(font, KivaGLFontType):
kiva_gl_font = font
font = Font(
face_name=kiva_gl_font.name,
size=kiva_gl_font.size,
family=kiva_gl_font.family,
style=kiva_gl_font.style,
)
bold = font.is_bold()
italic = font.style in italic_styles
pyglet_font = load_font(
font.findfontname(), font.size, bold, italic
)
GlobalFontCache[key] = pyglet_font
else:
pyglet_font = GlobalFontCache[key]
return pyglet_font
# Because Pyglet 1.1 uses persistent Label objects to efficiently lay
# out and render text, we cache these globally to minimize the creation
# time. An MRU is not really the right structure to use here, though.
# (We typically expect that the same numbers of labels will be rendered.)
GlobalTextCache = MRU()
GlobalTextCache.__maxlength__ = 100
def GetLabel(text, pyglet_font):
""" Returns a Pyglet Label object for the given text and font """
key = (text, pyglet_font)
if key not in GlobalTextCache:
# Use anchor_y="bottom" because by default, pyglet sets the
# baseline to the y coordinate given. Unfortunately, it doesn't
# expose a per-Text descent (only a per-Font descent), so it's
# impossible to know how to offset the y value properly for a
# given string.
label = Label(
text,
font_name=pyglet_font.name,
font_size=pyglet_font.size,
anchor_y="bottom",
)
GlobalTextCache[key] = label
else:
label = GlobalTextCache[key]
return label
except ImportError as exc:
# Pyglet is not available, so we forgo some features
logger.exception(
"Error importing Pyglet, some features not available in gl backend"
)
ArrayImage = None
GetFont = None
GetLabel = None
gl = None
class GraphicsContext(GraphicsContextGL):
def __init__(self, size, *args, **kw):
# Ignore the pix_format argument for now
kw.pop("pix_format", None)
base_scale = kw.pop("base_pixel_scale", 1)
GraphicsContextGL.__init__(self, size[0], size[1], *args, **kw)
self.corner_pixel_origin = True
# For HiDPI support
self.scale_ctm(base_scale, base_scale)
self._font_stack = []
self._current_font = None
self._text_pos = (0, 0)
def save_state(self):
super().save_state()
self._font_stack.append(self._current_font)
def restore_state(self):
super().restore_state()
self._current_font = self._font_stack.pop()
def set_font(self, font):
self._current_font = font
def get_text_extent(self, text):
if self._current_font is None:
return (0, 0, 0, 0)
pyglet_font = GetFont(self._current_font)
label = GetLabel(text, pyglet_font)
return (0, 0, label.content_width, label.content_height)
def set_text_position(self, x, y):
self._text_pos = (x, y)
def show_text(self, text, point=None):
if point is None:
point = self._text_pos
return self.show_text_at_point(text, *point)
def show_text_at_point(self, text, x, y):
if self._current_font is None:
return
pyglet_font = GetFont(self._current_font)
label = GetLabel(text, pyglet_font)
xform = self.get_ctm()
x0 = xform[4]
y0 = xform[5]
# The GL backend places the center of a pixel at (0.5, 0.5); however,
# for show_text_at_point, we don't actually want to render the text
# offset by half a pixel. There is probably a better, more uniform way
# to handle this across all of Kiva, because this is probably a common
# issue that will arise, but for now, we just round the position down.
x = floor(x + x0)
y = floor(y + y0)
label.x = x
label.y = y
c = self.get_fill_color()
label.color = (
int(c[0] * 255),
int(c[1] * 255),
int(c[2] * 255),
int(c[3] * 255),
)
label.draw()
return True
def linear_gradient(self, x1, y1, x2, y2, stops, spread_method,
units="userSpaceOnUse"):
""" Not implemented.
"""
pass
def radial_gradient(self, cx, cy, r, fx, fy, stops, spread_method,
units="userSpaceOnUse"):
""" Not implemented.
"""
pass
def draw_image(self, img, rect=None, force_copy=False):
""" Renders a GraphicsContextArray into this GC """
xform = self.get_ctm()
image = image_as_array(img)
shape = image.shape
if shape[2] == 4:
fmt = "RGBA"
else:
fmt = "RGB"
aii = ArrayImage(image, format=fmt)
texture = aii.texture
# The texture coords consists of (u,v,r) for each corner of the
# texture rectangle. The coordinates are stored in the order
# bottom left, bottom right, top right, top left.
x, y, w, h = rect
texture.width = w
texture.height = h
t = texture.tex_coords
points = array([[x, y + h], [x + w, y + h], [x + w, y], [x, y]])
p = transform_points(affine_from_values(*xform), points)
a = (gl.GLfloat*32)(
t[0], t[1], t[2], 1.,
p[0, 0], p[0, 1], 0, 1.,
t[3], t[4], t[5], 1.,
p[1, 0], p[1, 1], 0, 1.,
t[6], t[7], t[8], 1.,
p[2, 0], p[2, 1], 0, 1.,
t[9], t[10], t[11], 1.,
p[3, 0], p[3, 1], 0, 1.,
)
gl.glPushAttrib(gl.GL_ENABLE_BIT)
gl.glEnable(texture.target)
gl.glBindTexture(texture.target, texture.id)
gl.glPushClientAttrib(gl.GL_CLIENT_VERTEX_ARRAY_BIT)
gl.glInterleavedArrays(gl.GL_T4F_V4F, 0, a)
gl.glDrawArrays(gl.GL_QUADS, 0, 4)
gl.glPopClientAttrib()
gl.glPopAttrib()
def font_metrics_provider():
return GraphicsContext((1, 1))