CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
internals.py253 linesDownload Raw Back to Qt
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 
Aluode/PerceptionLabPortable · CoolFace