From c4983874591d324ea25a4632e891400bee5b2b77 Mon Sep 17 00:00:00 2001 From: unknown <geo_beja@PC-12-3465.geo.hu-berlin.de> Date: Tue, 10 Nov 2015 14:53:49 +0100 Subject: [PATCH] included qimage2ndarray from https://github.com/hmeine/qimage2ndarray --- .idea/vcs.xml | 2 +- qimage2ndarray/LICENSE.txt | 30 +++ qimage2ndarray/__init__.py | 403 ++++++++++++++++++++++++++++ qimage2ndarray/dynqt.py | 4 + qimage2ndarray/qimageview_python.py | 60 +++++ qimage2ndarray/qt_driver.py | 125 +++++++++ 6 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 qimage2ndarray/LICENSE.txt create mode 100644 qimage2ndarray/__init__.py create mode 100644 qimage2ndarray/dynqt.py create mode 100644 qimage2ndarray/qimageview_python.py create mode 100644 qimage2ndarray/qt_driver.py diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7f..35eb1ddf 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$" vcs="Git" /> + <mapping directory="" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/qimage2ndarray/LICENSE.txt b/qimage2ndarray/LICENSE.txt new file mode 100644 index 00000000..96d599da --- /dev/null +++ b/qimage2ndarray/LICENSE.txt @@ -0,0 +1,30 @@ +Copyright (c) 2009, Hans Meine <hans_meine@gmx.net> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +3. Neither the name of the University of Hamburg nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/qimage2ndarray/__init__.py b/qimage2ndarray/__init__.py new file mode 100644 index 00000000..6e56e0e0 --- /dev/null +++ b/qimage2ndarray/__init__.py @@ -0,0 +1,403 @@ +import sys as _sys +import numpy as _np + +from .dynqt import QtGui as _qt + +from .dynqt import qt as _qt_driver +if _qt_driver.name() == 'PythonQt': + from .qimageview import QImage2ndarray as _temp + _qimageview = _temp.qimageview +else: + from .qimageview_python import qimageview as _qimageview + +__version__ = "1.5" + +if _sys.byteorder == 'little': + _bgra = (0, 1, 2, 3) +else: + _bgra = (3, 2, 1, 0) + +_bgra_fields = {'b': (_np.uint8, _bgra[0], 'blue'), + 'g': (_np.uint8, _bgra[1], 'green'), + 'r': (_np.uint8, _bgra[2], 'red'), + 'a': (_np.uint8, _bgra[3], 'alpha')} + +bgra_dtype = _np.dtype(_bgra_fields) +"""Complex dtype offering the named fields 'r','g','b', and 'a' and +corresponding long names, conforming to QImage_'s 32-bit memory layout.""" + +try: + _basestring = basestring +except NameError: + # 'basestring' undefined, must be Python 3 + _basestring = str + +def _qimage_or_filename_view(qimage): + if isinstance(qimage, _basestring): + qimage = _qt.QImage(qimage) + return _qimageview(qimage) + +def raw_view(qimage): + """Returns raw 2D view of the given QImage_'s memory. The result + will be a 2-dimensional numpy.ndarray with an appropriately sized + integral dtype. (This function is not intented to be used + directly, but used internally by the other -- more convenient -- + view creation functions.) + + :param qimage: image whose memory shall be accessed via NumPy + :type qimage: QImage_ + :rtype: numpy.ndarray_ with shape (height, width)""" + return _qimage_or_filename_view(qimage) + + +def byte_view(qimage, byteorder = 'little'): + """Returns raw 3D view of the given QImage_'s memory. This will + always be a 3-dimensional numpy.ndarray with dtype numpy.uint8. + + Note that for 32-bit images, the last dimension will be in the + [B,G,R,A] order (if little endian) due to QImage_'s memory layout + (the alpha channel will be present for Format_RGB32 images, too). + + For 8-bit (indexed) images, the array will still be 3-dimensional, + i.e. shape will be (height, width, 1). + + The order of channels in the last axis depends on the `byteorder`, + which defaults to 'little', i.e. BGRA order. You may set the + argument `byteorder` to 'big' to get ARGB, or use None which means + sys.byteorder here, i.e. return native order for the machine the + code is running on. + + For your convenience, `qimage` may also be a filename, see + `Loading and Saving Images`_ in the documentation. + + :param qimage: image whose memory shall be accessed via NumPy + :type qimage: QImage_ + :param byteorder: specify order of channels in last axis + :rtype: numpy.ndarray_ with shape (height, width, 1 or 4) and dtype uint8""" + raw = _qimage_or_filename_view(qimage) + result = raw.view(_np.uint8).reshape(raw.shape + (-1, )) + if byteorder and byteorder != _sys.byteorder: + result = result[...,::-1] + return result + + +def rgb_view(qimage, byteorder = 'big'): + """Returns RGB view of a given 32-bit color QImage_'s memory. + Similarly to byte_view(), the result is a 3D numpy.uint8 array, + but reduced to the rgb dimensions (without alpha), and reordered + (using negative strides in the last dimension) to have the usual + [R,G,B] order. The image must have 32 bit pixel size, i.e. be + RGB32, ARGB32, or ARGB32_Premultiplied. (Note that in the latter + case, the values are of course premultiplied with alpha.) + + The order of channels in the last axis depends on the `byteorder`, + which defaults to 'big', i.e. RGB order. You may set the argument + `byteorder` to 'little' to get BGR, or use None which means + sys.byteorder here, i.e. return native order for the machine the + code is running on. + + For your convenience, `qimage` may also be a filename, see + `Loading and Saving Images`_ in the documentation. + + :param qimage: image whose memory shall be accessed via NumPy + :type qimage: QImage_ with 32-bit pixel type + :param byteorder: specify order of channels in last axis + :rtype: numpy.ndarray_ with shape (height, width, 3) and dtype uint8""" + if byteorder is None: + byteorder = _sys.byteorder + bytes = byte_view(qimage, byteorder) + if bytes.shape[2] != 4: + raise ValueError("For rgb_view, the image must have 32 bit pixel size (use RGB32, ARGB32, or ARGB32_Premultiplied)") + + if byteorder == 'little': + return bytes[...,:3] # strip A off BGRA + else: + return bytes[...,1:] # strip A off ARGB + + +def alpha_view(qimage): + """Returns alpha view of a given 32-bit color QImage_'s memory. + The result is a 2D numpy.uint8 array, equivalent to + byte_view(qimage)[...,3]. The image must have 32 bit pixel size, + i.e. be RGB32, ARGB32, or ARGB32_Premultiplied. Note that it is + not enforced that the given qimage has a format that actually + *uses* the alpha channel -- for Format_RGB32, the alpha channel + usually contains 255 everywhere. + + For your convenience, `qimage` may also be a filename, see + `Loading and Saving Images`_ in the documentation. + + :param qimage: image whose memory shall be accessed via NumPy + :type qimage: QImage_ with 32-bit pixel type + :rtype: numpy.ndarray_ with shape (height, width) and dtype uint8""" + bytes = byte_view(qimage, byteorder = None) + if bytes.shape[2] != 4: + raise ValueError("For alpha_view, the image must have 32 bit pixel size (use RGB32, ARGB32, or ARGB32_Premultiplied)") + return bytes[...,_bgra[3]] + + +def recarray_view(qimage): + """Returns recarray_ view of a given 32-bit color QImage_'s + memory. + + The result is a 2D array with a complex record dtype, offering the + named fields 'r','g','b', and 'a' and corresponding long names. + Thus, each color components can be accessed either via string + indexing or via attribute lookup (through numpy.recarray_): + + For your convenience, `qimage` may also be a filename, see + `Loading and Saving Images`_ in the documentation. + + >>> from PyQt4.QtGui import QImage, qRgb + >>> qimg = QImage(320, 240, QImage.Format_ARGB32) + >>> qimg.fill(qRgb(12,34,56)) + >>> + >>> import qimage2ndarray + >>> v = qimage2ndarray.recarray_view(qimg) + >>> + >>> red = v["r"] + >>> red[10,10] + 12 + >>> pixel = v[10,10] + >>> pixel["r"] + 12 + >>> (v.g == v["g"]).all() + True + >>> (v.alpha == 255).all() + True + + :param qimage: image whose memory shall be accessed via NumPy + :type qimage: QImage_ with 32-bit pixel type + :rtype: numpy.ndarray_ with shape (height, width) and dtype :data:`bgra_dtype`""" + raw = _qimage_or_filename_view(qimage) + if raw.itemsize != 4: + raise ValueError("For rgb_view, the image must have 32 bit pixel size (use RGB32, ARGB32, or ARGB32_Premultiplied)") + return raw.view(bgra_dtype, _np.recarray) + + + +def _normalize255(array, normalize, clip = (0, 255)): + if normalize: + if normalize is True: + normalize = array.min(), array.max() + if clip == (0, 255): + clip = None + elif _np.isscalar(normalize): + normalize = (0, normalize) + + nmin, nmax = normalize + + if nmin: + array = array - nmin + + if nmax != nmin: + scale = 255. / (nmax - nmin) + if scale != 1.0: + array = array * scale + + if clip: + low, high = clip + _np.clip(array, low, high, array) + + return array + + +def gray2qimage(gray, normalize = False): + """Convert the 2D numpy array `gray` into a 8-bit, indexed QImage_ + with a gray colormap. The first dimension represents the vertical + image axis. + + The parameter `normalize` can be used to normalize an image's + value range to 0..255: + + `normalize` = (nmin, nmax): + scale & clip image values from nmin..nmax to 0..255 + + `normalize` = nmax: + lets nmin default to zero, i.e. scale & clip the range 0..nmax + to 0..255 + + `normalize` = True: + scale image values to 0..255 (same as passing (gray.min(), + gray.max())) + + If the source array `gray` contains masked values, the result will + have only 255 shades of gray, and one color map entry will be used + to make the corresponding pixels transparent. + + A full alpha channel cannot be supported with indexed images; + instead, use `array2qimage` to convert into a 32-bit QImage. + + :param gray: image data which should be converted (copied) into a QImage_ + :type gray: 2D or 3D numpy.ndarray_ or `numpy.ma.array <masked arrays>`_ + :param normalize: normalization parameter (see above, default: no value changing) + :type normalize: bool, scalar, or pair + :rtype: QImage_ with RGB32 or ARGB32 format""" + if _np.ndim(gray) != 2: + raise ValueError("gray2QImage can only convert 2D arrays" + + " (try using array2qimage)" if _np.ndim(gray) == 3 else "") + + h, w = gray.shape + result = _qt.QImage(w, h, _qt.QImage.Format_Indexed8) + + if not _np.ma.is_masked(gray): + for i in range(256): + result.setColor(i, _qt.qRgb(i,i,i)) + + _qimageview(result)[:] = _normalize255(gray, normalize) + else: + # map gray value 1 to gray value 0, in order to make room for + # transparent colormap entry: + result.setColor(0, _qt.qRgb(0,0,0)) + for i in range(2, 256): + result.setColor(i-1, _qt.qRgb(i,i,i)) + + _qimageview(result)[:] = _normalize255(gray, normalize, clip = (1, 255)) - 1 + + result.setColor(255, 0) + _qimageview(result)[gray.mask] = 255 + + return result + + +def array2qimage(array, normalize = False): + """Convert a 2D or 3D numpy array into a 32-bit QImage_. The + first dimension represents the vertical image axis; the optional + third dimension is supposed to contain 1-4 channels: + + ========= =================== + #channels interpretation + ========= =================== + 1 scalar/gray + 2 scalar/gray + alpha + 3 RGB + 4 RGB + alpha + ========= =================== + + Scalar data will be converted into corresponding gray RGB triples; + if you want to convert to an (indexed) 8-bit image instead, use + `gray2qimage` (which cannot support an alpha channel though). + + The parameter `normalize` can be used to normalize an image's + value range to 0..255: + + `normalize` = (nmin, nmax): + scale & clip image values from nmin..nmax to 0..255 + + `normalize` = nmax: + lets nmin default to zero, i.e. scale & clip the range 0..nmax + to 0..255 + + `normalize` = True: + scale image values to 0..255 (same as passing (array.min(), + array.max())) + + If `array` contains masked values, the corresponding pixels will + be transparent in the result. Thus, the result will be of + QImage.Format_ARGB32 if the input already contains an alpha + channel (i.e. has shape (H,W,4)) or if there are masked pixels, + and QImage.Format_RGB32 otherwise. + + :param array: image data which should be converted (copied) into a QImage_ + :type array: 2D or 3D numpy.ndarray_ or `numpy.ma.array <masked arrays>`_ + :param normalize: normalization parameter (see above, default: no value changing) + :type normalize: bool, scalar, or pair + :rtype: QImage_ with RGB32 or ARGB32 format""" + if _np.ndim(array) == 2: + array = array[...,None] + elif _np.ndim(array) != 3: + raise ValueError("array2qimage can only convert 2D or 3D arrays (got %d dimensions)" % _np.ndim(array)) + if array.shape[2] not in (1, 2, 3, 4): + raise ValueError("array2qimage expects the last dimension to contain exactly one (scalar/gray), two (gray+alpha), three (R,G,B), or four (R,G,B,A) channels") + + h, w, channels = array.shape + + hasAlpha = _np.ma.is_masked(array) or channels in (2, 4) + fmt = _qt.QImage.Format_ARGB32 if hasAlpha else _qt.QImage.Format_RGB32 + + result = _qt.QImage(w, h, fmt) + + array = _normalize255(array, normalize) + + if channels >= 3: + rgb_view(result)[:] = array[...,:3] + else: + rgb_view(result)[:] = array[...,:1] # scalar data + + alpha = alpha_view(result) + + if channels in (2, 4): + alpha[:] = array[...,-1] + else: + alpha[:] = 255 + + if _np.ma.is_masked(array): + alpha[:] *= _np.logical_not(_np.any(array.mask, axis = -1)) + + return result + + +def imread(filename, masked = False): + """Convenience function that uses the QImage_ constructor to read an + image from the given file and return an `rgb_view` of the result. + This is intentionally similar to scipy.ndimage.imread (which uses + PIL), scipy.misc.imread, or matplotlib.pyplot.imread (using PIL + for non-PNGs). + + For grayscale images, return 2D array (even if it comes from a 32-bit + representation; this is a consequence of the QImage API). + + For images with an alpha channel, the resulting number of channels + will be 2 (grayscale+alpha) or 4 (RGB+alpha). Alternatively, one may + pass `masked = True' in order to get `numpy.ma.array <masked + arrays>`_ back. Note that only fully transparent pixels are masked + (and that masked arrays only support binary masks). The value of + `masked` is ignored when the loaded image has no alpha channel + (i.e., one would not get a masked array in that case). + + This function has been added in version 1.3. + + """ + qImage = _qt.QImage(filename) + + isGray = qImage.isGrayscale() + if isGray and qImage.depth() == 8: + return byte_view(qImage)[...,0] + + hasAlpha = qImage.hasAlphaChannel() + + if hasAlpha: + targetFormat = _qt.QImage.Format_ARGB32 + else: + targetFormat = _qt.QImage.Format_RGB32 + if qImage.format() != targetFormat: + qImage = qImage.convertToFormat(targetFormat) + + result = rgb_view(qImage) + if isGray: + result = result[...,0] + if hasAlpha: + if masked: + mask = (alpha_view(qImage) == 0) + if _np.ndim(result) == 3: + mask = _np.repeat(mask[...,None], 3, axis = 2) + result = _np.ma.masked_array(result, mask) + else: + result = _np.dstack((result, alpha_view(qImage))) + return result + + +def imsave(filename, image, normalize = False, format = None, quality = -1): + """Convenience function that uses QImage.save to save an image to the + given file. This is intentionally similar to scipy.misc.imsave. + However, it supports different optional arguments: + + :param normalize: see :func:`array2qimage` (which is used internally) + :param format: image filetype (e.g. 'PNG'), (default: check filename's suffix) + :param quality: see QImage.save (0 = small .. 100 = uncompressed, -1 = default compression) + :returns: boolean success, see QImage.save + + This function has been added in version 1.4. + """ + qImage = array2qimage(image, normalize = normalize) + return qImage.save(filename, format, quality) diff --git a/qimage2ndarray/dynqt.py b/qimage2ndarray/dynqt.py new file mode 100644 index 00000000..a4f1a18b --- /dev/null +++ b/qimage2ndarray/dynqt.py @@ -0,0 +1,4 @@ +from .qt_driver import QtDriver + +qt = QtDriver() +QtGui = qt.QtGui diff --git a/qimage2ndarray/qimageview_python.py b/qimage2ndarray/qimageview_python.py new file mode 100644 index 00000000..83efb2dd --- /dev/null +++ b/qimage2ndarray/qimageview_python.py @@ -0,0 +1,60 @@ +import numpy as np +from qimage2ndarray.dynqt import qt, QtGui + +def PyQt_data(image): + # PyQt4/PyQt5's QImage.bits() returns a sip.voidptr that supports + # conversion to string via asstring(size) or getting its base + # address via int(...): + return (int(image.bits()), False) + +def _re_buffer_address_match(buf_repr): + import re + _re_buffer_address = re.compile('<read-write buffer ptr 0x([0-9a-fA-F]*),') + global _re_buffer_address_match + _re_buffer_address_match = _re_buffer_address.match + return _re_buffer_address_match(buf_repr) + +def PySide_data(image): + # PySide's QImage.bits() returns a buffer object like this: + # <read-write buffer ptr 0x7fc3f4821600, size 76800 at 0x111269570> + ma = _re_buffer_address_match(repr(image.bits())) + assert ma, 'could not parse address from %r' % (image.bits(), ) + return (int(ma.group(1), 16), False) + +getdata = dict( + PyQt4 = PyQt_data, + PyQt5 = PyQt_data, + PySide = PySide_data, +)[qt.name()] + + +def qimageview(image): + if not isinstance(image, QtGui.QImage): + raise TypeError("image argument must be a QImage instance") + + shape = image.height(), image.width() + strides0 = image.bytesPerLine() + + format = image.format() + if format == QtGui.QImage.Format_Indexed8: + dtype = "|u1" + strides1 = 1 + elif format in (QtGui.QImage.Format_RGB32, QtGui.QImage.Format_ARGB32, QtGui.QImage.Format_ARGB32_Premultiplied): + dtype = "|u4" + strides1 = 4 + elif format == QtGui.QImage.Format_Invalid: + raise ValueError("qimageview got invalid QImage") + else: + raise ValueError("qimageview can only handle 8- or 32-bit QImages") + + image.__array_interface__ = { + 'shape': shape, + 'typestr': dtype, + 'data': getdata(image), + 'strides': (strides0, strides1), + 'version': 3, + } + + result = np.asarray(image) + del image.__array_interface__ + return result diff --git a/qimage2ndarray/qt_driver.py b/qimage2ndarray/qt_driver.py new file mode 100644 index 00000000..483cd287 --- /dev/null +++ b/qimage2ndarray/qt_driver.py @@ -0,0 +1,125 @@ +# Copyright 2014-2014 Hans Meine <hans_meine@gmx.net> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module contains a wrapper around three different Qt python bindings. +It will dynamically decide which one to use: + +* First, the environment variable QT_DRIVER is checked + (may be one of 'PyQt5', 'PyQt4', 'PySide', 'PythonQt'). +* If unset, previously imported binding modules are detected (in sys.modules). +* If no bindings are loaded, the environment variable QT_API is checked + (used by ETS and ipython, may be 'pyside' or 'pyqt'). + +In order to have compatible behavior between the different bindings, +PyQt4 (if used) is configured as follows:: + + sip.setapi("QString", 2) + sip.setapi("QVariant", 2) + +Furthermore, there is a 'getprop' function that solves the following +problem: PythonQt exports Qt properties as Python properties *and* +gives the precedence over getters with the same name. Instead of +calling getters with parentheses (which must not be used in PythonQt, +but are required in PyQt and PySide), one may e.g. write +`getprop(widget.width)`. +""" + +import sys, os + +def getprop_PythonQt(prop): + """getprop(property_or_getter) + + Used on getters that have the same name as a corresponding + property. For PythonQt, this version will just return the + argument, which is assumed to be (the value of) a python property + through which PythonQt exposes Qt properties.""" + return prop + +def getprop_other(getter): + """getprop(property_or_getter) + + Used on getters that have the same name as a corresponding + property. For Qt bindings other than PythonQt, this version will + return the result of calling the argument, which is assumed to be + a Qt getter function. (With PythonQt, properties override getters + and no calling must be done.)""" + return getter() + +class QtDriver(object): + DRIVERS = ('PyQt5', 'PyQt4', 'PySide', 'PythonQt') + DEFAULT = 'PyQt4' + + @classmethod + def detect_qt(cls): + for drv in cls.DRIVERS: + if drv in sys.modules: + return drv + if '_PythonQt' in sys.modules: + return 'PythonQt' + return None + + def name(self): + return self._drv + + def getprop(self): + return getprop_PythonQt if self._drv == 'PythonQt' else getprop_other + + def __init__(self, drv = os.environ.get('QT_DRIVER')): + """Supports QT_API (used by ETS and ipython)""" + if drv is None: + drv = self.detect_qt() + if drv is None: + drv = os.environ.get('QT_API') + if drv is None: + drv = self.DEFAULT + drv = {'pyside' : 'PySide', 'pyqt' : 'PyQt4', 'pyqt5' : 'PyQt5'}.get(drv, drv) # map ETS syntax + assert drv in self.DRIVERS + self._drv = drv + + @staticmethod + def _initPyQt4(): + """initialize PyQt4 to be compatible with PySide""" + if 'PyQt4.QtCore' in sys.modules: + # too late to configure API + pass + else: + import sip + sip.setapi("QString", 2) + sip.setapi("QVariant", 2) + + @staticmethod + def requireCompatibleAPI(): + """If PyQt4's API should be configured to be compatible with PySide's + (i.e. QString and QVariant should not be explicitly exported, + cf. documentation of sip.setapi()), call this function to check that + the PyQt4 was properly imported. (It will always be configured this + way by this module, but it could have been imported before we got a + hand on doing so.) + """ + if 'PyQt4.QtCore' in sys.modules: + import sip + for api in ('QVariant', 'QString'): + if sip.getapi(api) != 2: + raise RuntimeError('%s API already set to V%d, but should be 2' % (api, sip.getapi(api))) + + def importMod(self, mod): + if self._drv == 'PyQt4': + self._initPyQt4() + qt = __import__('%s.%s' % (self._drv, mod)) + return getattr(qt, mod) + + def __getattr__(self, name): + if name.startswith('Qt'): + return self.importMod(name) + return super(QtDriver, self).__getattr__(name) -- GitLab