Aluode/PerceptionLabPortable
0
1import ctypes2import itertools3import sys4 5import numpy as np6 7from . import QT_LIB, QtCore, QtGui, compat8 9__all__ = ["get_qpainterpath_element_array"]10 11if QT_LIB.startswith('PyQt'):12 from . import sip13 qt_version_info = tuple((QtCore.QT_VERSION >> i) & 0xff for i in [16,8,0])14elif QT_LIB == 'PySide2':15 from PySide2 import __version_info__ as pyside_version_info16 qt_version_info = QtCore.__version_info__17elif QT_LIB == 'PySide6':18 from PySide6 import __version_info__ as pyside_version_info19 qt_version_info = QtCore.__version_info__20 21 22class Element(ctypes.Structure):23 _fields_= [('x', ctypes.c_double), ('y', ctypes.c_double), ('c', ctypes.c_int)]24 25class QArrayData(ctypes.Structure):26 pass27 28class QPainterPathPrivate(ctypes.Structure):29 pass30 31if qt_version_info[0] == 5:32 QArrayData._fields_ = [33 ("ref", ctypes.c_int),34 ("size", ctypes.c_int),35 ("alloc", ctypes.c_uint, 31),36 ("offset", ctypes.c_ssize_t),37 ]38 39 QPainterPathPrivate._fields_ = [40 ("ref", ctypes.c_int),41 ("adata", ctypes.POINTER(QArrayData)),42 ]43 44elif qt_version_info[0] == 6:45 QArrayData._fields_ = [46 ("ref", ctypes.c_int),47 ("flags", ctypes.c_uint),48 ("alloc", ctypes.c_ssize_t),49 ]50 51 QPainterPathPrivate._fields_ = [52 ("ref", ctypes.c_int),53 ("adata", ctypes.POINTER(QArrayData)),54 ("data", ctypes.c_void_p),55 ("size", ctypes.c_ssize_t),56 ][int(qt_version_info >= (6, 10)):]57 58def get_qpainterpath_element_array(qpath, nelems=None):59 resize = nelems is not None60 if resize:61 qpath.reserve(nelems)62 63 ptr = ctypes.c_void_p.from_address(compat.unwrapinstance(qpath))64 if not ptr:65 return np.zeros(0, dtype=Element)66 67 ppp = ctypes.cast(ptr, ctypes.POINTER(QPainterPathPrivate)).contents68 69 if qt_version_info[0] == 5:70 qad = ppp.adata.contents71 eptr = ctypes.addressof(qad) + qad.offset72 if resize:73 qad.size = nelems74 elif qt_version_info[0] == 6:75 eptr = ppp.data76 if resize:77 ppp.size = nelems78 else:79 raise NotImplementedError80 81 nelems = qpath.elementCount()82 buf = (Element * nelems).from_address(eptr)83 return np.frombuffer(buf, dtype=Element)84 85class PrimitiveArray:86 # Note: This class is an internal implementation detail and is not part87 # of the public API.88 #89 # QPainter has a C++ native API that takes an array of objects:90 # drawPrimitives(const Primitive *array, int count, ...)91 # where "Primitive" is one of QPointF, QLineF, QRectF, PixmapFragment92 #93 # PySide (with the exception of drawPixmapFragments) and older PyQt94 # require a Python list of "Primitive" instances to be provided to95 # the respective "drawPrimitives" method.96 #97 # This is inefficient because:98 # 1) constructing the Python list involves calling wrapinstance multiple times.99 # - this is mitigated here by reusing the instance pointers100 # 2) The binding will anyway have to repack the instances into a contiguous array,101 # in order to call the underlying C++ native API.102 #103 # Newer PyQt provides sip.array, which is more efficient.104 #105 # PySide's drawPixmapFragments() takes an instance to the first item of a106 # C array of PixmapFragment(s) _and_ the length of the array.107 # There is no overload that takes a Python list of PixmapFragment(s).108 109 def __init__(self, Klass, nfields, *, use_array=None):110 self._Klass = Klass111 self._nfields = nfields112 self._capa = -1113 114 self.use_sip_array = False115 self.use_ptr_to_array = False116 117 if QT_LIB.startswith('PyQt'):118 if use_array is None:119 use_array = (120 hasattr(sip, 'array') and121 (122 (0x60301 <= QtCore.PYQT_VERSION) or123 (0x50f07 <= QtCore.PYQT_VERSION < 0x60000)124 )125 )126 self.use_sip_array = use_array127 128 if QT_LIB.startswith('PySide'):129 if use_array is None:130 use_array = (131 Klass is QtGui.QPainter.PixmapFragment132 or pyside_version_info >= (6, 4, 3)133 )134 self.use_ptr_to_array = use_array135 136 self.resize(0)137 138 def resize(self, size):139 if self.use_sip_array:140 # For reference, SIP_VERSION 6.7.8 first arrived141 # in PyQt5_sip 12.11.2 and PyQt6_sip 13.4.2142 if sip.SIP_VERSION >= 0x60708:143 if size <= self._capa:144 self._size = size145 return146 else:147 # sip.array prior to SIP_VERSION 6.7.8 had a148 # buggy slicing implementation.149 # so trigger a reallocate for any different size150 if size == self._capa:151 return152 153 self._siparray = sip.array(self._Klass, size)154 155 else:156 if size <= self._capa:157 self._size = size158 return159 self._ndarray = np.empty((size, self._nfields), dtype=np.float64)160 161 if self.use_ptr_to_array:162 # defer creation163 self._objs = None164 else:165 self._objs = self._wrap_instances(self._ndarray)166 167 self._capa = size168 self._size = size169 170 def _wrap_instances(self, array):171 return list(map(compat.wrapinstance,172 itertools.count(array.ctypes.data, array.strides[0]),173 itertools.repeat(self._Klass, array.shape[0])))174 175 def __len__(self):176 return self._size177 178 def ndarray(self):179 # ndarray views are cheap to recreate each time180 if self.use_sip_array:181 # sip.array prior to SIP_VERSION 6.7.8 had a buggy buffer protocol182 # that set the wrong size.183 # workaround it by going through a sip.voidptr184 mv = sip.voidptr(self._siparray, self._capa*self._nfields*8)185 # note that we perform the slicing by using only _size rows186 nd = np.frombuffer(mv, dtype=np.float64, count=self._size*self._nfields)187 return nd.reshape((-1, self._nfields))188 else:189 return self._ndarray[:self._size]190 191 def instances(self):192 # this returns an iterable container of Klass instances.193 # for "use_ptr_to_array" mode, such a container may not194 # be required at all, so its creation is deferred195 if self.use_sip_array:196 if self._size == self._capa:197 # avoiding slicing when it's not necessary198 # handles the case where sip.array had a buggy199 # slicing implementation 200 return self._siparray201 else:202 # this is a view203 return self._siparray[:self._size]204 205 if self._objs is None:206 self._objs = self._wrap_instances(self._ndarray)207 208 if self._size == self._capa:209 return self._objs210 else:211 # this is a shallow copy212 return self._objs[:self._size]213 214 def drawargs(self):215 # returns arguments to apply to the respective drawPrimitives() functions216 if self.use_ptr_to_array:217 if self._capa > 0:218 # wrap memory only if it is safe to do so219 ptr = compat.wrapinstance(self._ndarray.ctypes.data, self._Klass)220 else:221 # shiboken translates None <--> nullptr222 # alternatively, we could instantiate a dummy _Klass()223 ptr = None224 return ptr, self._size225 226 else:227 return self.instances(),228 229 230_qbytearray_leaks = None231 232def qbytearray_leaks() -> bool:233 global _qbytearray_leaks234 235 if _qbytearray_leaks is None:236 # When PySide{2,6} is built without Py_LIMITED_API,237 # it leaks memory when a memory view to a QByteArray238 # object is taken.239 # See https://github.com/pyqtgraph/pyqtgraph/issues/3265240 # and PYSIDE-3031241 # Note: official builds of PySide{2,6} by Qt are built with242 # the limited api, and thus do not leak.243 if QT_LIB.startswith("PySide"):244 # probe whether QByteArray leaks245 qba = QtCore.QByteArray()246 ref0 = sys.getrefcount(qba)247 memoryview(qba)248 _qbytearray_leaks = sys.getrefcount(qba) > ref0249 else:250 _qbytearray_leaks = False251 252 return _qbytearray_leaks253 