From bfa4dba15485b4192f8bbe13345e9658c97ecf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Sat, 7 Oct 2017 07:59:01 +0200 Subject: New upstream version 0.6.0+dfsg --- silx/gui/_glutils/FramebufferTexture.py | 78 +++--- silx/gui/_glutils/OpenGLWidget.py | 409 ++++++++++++++++++++++++++++++++ silx/gui/_glutils/VertexBuffer.py | 10 +- silx/gui/_glutils/__init__.py | 1 + silx/gui/_glutils/font.py | 32 ++- 5 files changed, 476 insertions(+), 54 deletions(-) create mode 100644 silx/gui/_glutils/OpenGLWidget.py (limited to 'silx/gui/_glutils') diff --git a/silx/gui/_glutils/FramebufferTexture.py b/silx/gui/_glutils/FramebufferTexture.py index b01eb41..cc05080 100644 --- a/silx/gui/_glutils/FramebufferTexture.py +++ b/silx/gui/_glutils/FramebufferTexture.py @@ -66,49 +66,48 @@ class FramebufferTexture(object): self._previousFramebuffer = 0 # Used by with statement self._name = gl.glGenFramebuffers(1) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._name) - - # Attachments - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, - gl.GL_COLOR_ATTACHMENT0, - gl.GL_TEXTURE_2D, - self._texture.name, - 0) - - height, width = self._texture.shape - - if stencilFormat is not None: - self._stencilId = gl.glGenRenderbuffers(1) - gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId) - gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, - stencilFormat, - width, height) - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, - gl.GL_STENCIL_ATTACHMENT, - gl.GL_RENDERBUFFER, - self._stencilId) - else: - self._stencilId = None - if depthFormat is not None: - if self._stencilId and depthFormat in self._PACKED_FORMAT: - self._depthId = self._stencilId - else: - self._depthId = gl.glGenRenderbuffers(1) - gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId) + with self: # Bind FBO + # Attachments + gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, + gl.GL_COLOR_ATTACHMENT0, + gl.GL_TEXTURE_2D, + self._texture.name, + 0) + + height, width = self._texture.shape + + if stencilFormat is not None: + self._stencilId = gl.glGenRenderbuffers(1) + gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId) gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, - depthFormat, + stencilFormat, width, height) - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, - gl.GL_DEPTH_ATTACHMENT, - gl.GL_RENDERBUFFER, - self._depthId) - else: - self._depthId = None + gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, + gl.GL_STENCIL_ATTACHMENT, + gl.GL_RENDERBUFFER, + self._stencilId) + else: + self._stencilId = None + + if depthFormat is not None: + if self._stencilId and depthFormat in self._PACKED_FORMAT: + self._depthId = self._stencilId + else: + self._depthId = gl.glGenRenderbuffers(1) + gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId) + gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, + depthFormat, + width, height) + gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, + gl.GL_DEPTH_ATTACHMENT, + gl.GL_RENDERBUFFER, + self._depthId) + else: + self._depthId = None - assert gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == \ - gl.GL_FRAMEBUFFER_COMPLETE - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) + assert (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == + gl.GL_FRAMEBUFFER_COMPLETE) @property def shape(self): @@ -143,6 +142,7 @@ class FramebufferTexture(object): def __exit__(self, exctype, excvalue, traceback): gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._previousFramebuffer) + self._previousFramebuffer = None def discard(self): """Delete associated OpenGL resources including texture""" diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py new file mode 100644 index 0000000..6cbf8f0 --- /dev/null +++ b/silx/gui/_glutils/OpenGLWidget.py @@ -0,0 +1,409 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This package provides a compatibility layer for OpenGL widget. + +It provides a compatibility layer for Qt OpenGL widget used in silx +across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "26/07/2017" + + +import logging +import sys + +from .. import qt +from .._glutils import gl + + +_logger = logging.getLogger(__name__) + + +# Probe OpenGL availability and widget +ERROR = '' # Error message from probing Qt OpenGL support +_BaseOpenGLWidget = None # Qt OpenGL widget to use + +if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4 + _logger.info('Using QOpenGLWidget') + _BaseOpenGLWidget = qt.QOpenGLWidget + +elif not qt.HAS_OPENGL: # QtOpenGL not installed + ERROR = '%s.QtOpenGL not available' % qt.BINDING + +elif qt.QApplication.instance() and not qt.QGLFormat.hasOpenGL(): + # qt.QGLFormat.hasOpenGL MUST be called with a QApplication created + # so this is only checked if the QApplication is already created + ERROR = 'Qt reports OpenGL not available' + +else: + _logger.info('Using QGLWidget') + _BaseOpenGLWidget = qt.QGLWidget + + +# Internal class wrapping Qt OpenGL widget +if _BaseOpenGLWidget is None: + _logger.error('OpenGL-based widget disabled: %s', ERROR) + _OpenGLWidget = None + +else: + class _OpenGLWidget(_BaseOpenGLWidget): + """Wrapper over QOpenGLWidget and QGLWidget""" + + sigOpenGLContextError = qt.Signal(str) + """Signal emitted when an OpenGL context error is detected at runtime. + + It provides the error reason as a str. + """ + + def __init__(self, parent, + alphaBufferSize=0, + depthBufferSize=24, + stencilBufferSize=8, + version=(2, 0), + f=qt.Qt.WindowFlags()): + # True if using QGLWidget, False if using QOpenGLWidget + self.__legacy = not hasattr(qt, 'QOpenGLWidget') + + self.__devicePixelRatio = 1.0 + self.__requestedOpenGLVersion = int(version[0]), int(version[1]) + self.__isValid = False + + if self.__legacy: # QGLWidget + format_ = qt.QGLFormat() + format_.setAlphaBufferSize(alphaBufferSize) + format_.setAlpha(alphaBufferSize != 0) + format_.setDepthBufferSize(depthBufferSize) + format_.setDepth(depthBufferSize != 0) + format_.setStencilBufferSize(stencilBufferSize) + format_.setStencil(stencilBufferSize != 0) + format_.setVersion(*self.__requestedOpenGLVersion) + format_.setDoubleBuffer(True) + + super(_OpenGLWidget, self).__init__(format_, parent, None, f) + + else: # QOpenGLWidget + super(_OpenGLWidget, self).__init__(parent, f) + + format_ = qt.QSurfaceFormat() + format_.setAlphaBufferSize(alphaBufferSize) + format_.setDepthBufferSize(depthBufferSize) + format_.setStencilBufferSize(stencilBufferSize) + format_.setVersion(*self.__requestedOpenGLVersion) + format_.setSwapBehavior(qt.QSurfaceFormat.DoubleBuffer) + self.setFormat(format_) + + + def getDevicePixelRatio(self): + """Returns the ratio device-independent / device pixel size + + It should be either 1.0 or 2.0. + + :return: Scale factor between screen and Qt units + :rtype: float + """ + return self.__devicePixelRatio + + def getRequestedOpenGLVersion(self): + """Returns the requested OpenGL version. + + :return: (major, minor) + :rtype: 2-tuple of int""" + return self.__requestedOpenGLVersion + + def getOpenGLVersion(self): + """Returns the available OpenGL version. + + :return: (major, minor) + :rtype: 2-tuple of int""" + if self.__legacy: # QGLWidget + supportedVersion = 0, 0 + + # Go through all OpenGL version flags checking support + flags = self.format().openGLVersionFlags() + for version in ((1, 1), (1, 2), (1, 3), (1, 4), (1, 5), + (2, 0), (2, 1), + (3, 0), (3, 1), (3, 2), (3, 3), + (4, 0)): + versionFlag = getattr(qt.QGLFormat, + 'OpenGL_Version_%d_%d' % version) + if not versionFlag & flags: + break + supportedVersion = version + return supportedVersion + + else: # QOpenGLWidget + return self.format().version() + + # QOpenGLWidget methods + + def isValid(self): + """Returns True if OpenGL is available. + + This adds extra checks to Qt isValid method. + + :rtype: bool + """ + return self.__isValid and super(_OpenGLWidget, self).isValid() + + def defaultFramebufferObject(self): + """Returns the framebuffer object handle. + + See :meth:`QOpenGLWidget.defaultFramebufferObject` + """ + if self.__legacy: # QGLWidget + return 0 + else: # QOpenGLWidget + return super(_OpenGLWidget, self).defaultFramebufferObject() + + # *GL overridden methods + + def initializeGL(self): + parent = self.parent() + if parent is None: + _logger.error('_OpenGLWidget has no parent') + return + + # Check OpenGL version + if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion(): + version = gl.glGetString(gl.GL_VERSION) + if version: + self.__isValid = True + else: + errMsg = 'OpenGL not available' + if sys.platform.startswith('linux'): + errMsg += ': If connected remotely, ' \ + 'GLX forwarding might be disabled.' + _logger.error(errMsg) + self.sigOpenGLContextError.emit(errMsg) + self.__isValid = False + + else: + errMsg = 'OpenGL %d.%d not available' % \ + self.getRequestedOpenGLVersion() + _logger.error('OpenGL widget disabled: %s', errMsg) + self.sigOpenGLContextError.emit(errMsg) + self.__isValid = False + + if self.isValid(): + parent.initializeGL() + + def paintGL(self): + parent = self.parent() + if parent is None: + _logger.error('_OpenGLWidget has no parent') + return + + if qt.BINDING == 'PyQt5': + devicePixelRatio = self.window().windowHandle().devicePixelRatio() + + if devicePixelRatio != self.getDevicePixelRatio(): + # Update devicePixelRatio and call resizeOpenGL + # as resizeGL is not always called. + self.__devicePixelRatio = devicePixelRatio + self.makeCurrent() + parent.resizeGL(self.width(), self.height()) + + if self.isValid(): + parent.paintGL() + + def resizeGL(self, width, height): + parent = self.parent() + if parent is None: + _logger.error('_OpenGLWidget has no parent') + return + + if self.isValid(): + # Call parent resizeGL with device-independent pixel unit + # This works over both QGLWidget and QOpenGLWidget + parent.resizeGL(self.width(), self.height()) + + +class OpenGLWidget(qt.QWidget): + """OpenGL widget wrapper over QGLWidget and QOpenGLWidget + + This wrapper API implements a subset of QOpenGLWidget API. + The constructor takes a different set of arguments. + Methods returning object like :meth:`context` returns either + QGL* or QOpenGL* objects. + + :param parent: Parent widget see :class:`QWidget` + :param int alphaBufferSize: + Size in bits of the alpha channel (default: 0). + Set to 0 to disable alpha channel. + :param int depthBufferSize: + Size in bits of the depth buffer (default: 24). + Set to 0 to disable depth buffer. + :param int stencilBufferSize: + Size in bits of the stencil buffer (default: 8). + Set to 0 to disable stencil buffer + :param version: Requested OpenGL version (default: (2, 0)). + :type version: 2-tuple of int + :param f: see :class:`QWidget` + """ + + def __init__(self, parent=None, + alphaBufferSize=0, + depthBufferSize=24, + stencilBufferSize=8, + version=(2, 0), + f=qt.Qt.WindowFlags()): + super(OpenGLWidget, self).__init__(parent, f) + + layout = qt.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + if _OpenGLWidget is None: + self.__openGLWidget = None + label = self._createErrorQLabel(ERROR) + self.layout().addWidget(label) + + else: + self.__openGLWidget = _OpenGLWidget( + parent=self, + alphaBufferSize=alphaBufferSize, + depthBufferSize=depthBufferSize, + stencilBufferSize=stencilBufferSize, + version=version, + f=f) + # Async connection need, otherwise issue when hiding OpenGL + # widget while doing the rendering.. + self.__openGLWidget.sigOpenGLContextError.connect( + self._handleOpenGLInitError, qt.Qt.QueuedConnection) + self.layout().addWidget(self.__openGLWidget) + + @staticmethod + def _createErrorQLabel(error): + """Create QLabel displaying error message in place of OpenGL widget + + :param str error: The error message to display""" + label = qt.QLabel() + label.setText('OpenGL-based widget disabled:\n%s' % error) + label.setAlignment(qt.Qt.AlignCenter) + label.setWordWrap(True) + return label + + def _handleOpenGLInitError(self, error): + """Handle runtime errors in OpenGL widget""" + if self.__openGLWidget is not None: + self.__openGLWidget.setVisible(False) + self.__openGLWidget.setParent(None) + self.__openGLWidget = None + + label = self._createErrorQLabel(error) + self.layout().addWidget(label) + + # Additional API + + def getDevicePixelRatio(self): + """Returns the ratio device-independent / device pixel size + + It should be either 1.0 or 2.0. + + :return: Scale factor between screen and Qt units + :rtype: float + """ + if self.__openGLWidget is None: + return 1. + else: + return self.__openGLWidget.getDevicePixelRatio() + + def getOpenGLVersion(self): + """Returns the available OpenGL version. + + :return: (major, minor) + :rtype: 2-tuple of int""" + if self.__openGLWidget is None: + return 0, 0 + else: + return self.__openGLWidget.getOpenGLVersion() + + # QOpenGLWidget API + + def isValid(self): + """Returns True if OpenGL with the requested version is available. + + :rtype: bool + """ + if self.__openGLWidget is None: + return False + else: + return self.__openGLWidget.isValid() + + def context(self): + """Return Qt OpenGL context object or None. + + See :meth:`QOpenGLWidget.context` and :meth:`QGLWidget.context` + """ + if self.__openGLWidget is None: + return None + else: + return self.__openGLWidget.context() + + def defaultFramebufferObject(self): + """Returns the framebuffer object handle. + + See :meth:`QOpenGLWidget.defaultFramebufferObject` + """ + if self.__openGLWidget is None: + return 0 + else: + return self.__openGLWidget.defaultFramebufferObject() + + def makeCurrent(self): + """Make the underlying OpenGL widget's context current. + + See :meth:`QOpenGLWidget.makeCurrent` + """ + if self.__openGLWidget is not None: + self.__openGLWidget.makeCurrent() + + def update(self): + """Async update of the OpenGL widget. + + See :meth:`QOpenGLWidget.update` + """ + if self.__openGLWidget is not None: + self.__openGLWidget.update() + + # QOpenGLWidget API to override + + def initializeGL(self): + """Override to implement OpenGL initialization.""" + pass + + def paintGL(self): + """Override to implement OpenGL rendering.""" + pass + + def resizeGL(self, width, height): + """Override to implement resize of OpenGL framebuffer. + + :param int width: Width in device-independent pixels + :param int height: Height in device-independent pixels + """ + pass diff --git a/silx/gui/_glutils/VertexBuffer.py b/silx/gui/_glutils/VertexBuffer.py index 689b543..b74b748 100644 --- a/silx/gui/_glutils/VertexBuffer.py +++ b/silx/gui/_glutils/VertexBuffer.py @@ -180,7 +180,7 @@ class VertexBufferAttrib(object): dimension=1, offset=0, stride=0, - normalisation=False): + normalization=False): self.vbo = vbo assert type_ in self._GL_TYPES self.type_ = type_ @@ -189,7 +189,7 @@ class VertexBufferAttrib(object): self.dimension = dimension self.offset = offset self.stride = stride - self.normalisation = bool(normalisation) + self.normalization = bool(normalization) @property def itemsize(self): @@ -200,12 +200,12 @@ class VertexBufferAttrib(object): def setVertexAttrib(self, attribute): """Call glVertexAttribPointer with objects information""" - normalisation = gl.GL_TRUE if self.normalisation else gl.GL_FALSE + normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE with self.vbo: gl.glVertexAttribPointer(attribute, self.dimension, self.type_, - normalisation, + normalization, self.stride, c_void_p(self.offset)) @@ -216,7 +216,7 @@ class VertexBufferAttrib(object): self.dimension, self.offset, self.stride, - self.normalisation) + self.normalization) def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py index e86a58f..15e48e1 100644 --- a/silx/gui/_glutils/__init__.py +++ b/silx/gui/_glutils/__init__.py @@ -33,6 +33,7 @@ __date__ = "25/07/2016" # OpenGL convenient functions +from .OpenGLWidget import OpenGLWidget # noqa from .Context import getGLContext, setGLContextGetter # noqa from .FramebufferTexture import FramebufferTexture # noqa from .Program import Program # noqa diff --git a/silx/gui/_glutils/font.py b/silx/gui/_glutils/font.py index 566ae49..2be2c04 100644 --- a/silx/gui/_glutils/font.py +++ b/silx/gui/_glutils/font.py @@ -98,27 +98,39 @@ def rasterText(text, font, _logger.info("Trying to raster empty text, replaced by white space") text = ' ' # Replace empty text by white space to produce an image + if (devicePixelRatio != 1.0 and + not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4 + _logger.error('devicePixelRatio not supported') + devicePixelRatio = 1.0 + if not isinstance(font, qt.QFont): font = qt.QFont(font, size, weight, italic) + # get text size + image = qt.QImage(1, 1, qt.QImage.Format_RGB888) + painter = qt.QPainter() + painter.begin(image) + painter.setPen(qt.Qt.white) + painter.setFont(font) + bounds = painter.boundingRect( + qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text) + painter.end() + metrics = qt.QFontMetrics(font) - size = metrics.size(qt.Qt.TextExpandTabs, text) - bounds = metrics.boundingRect( - qt.QRect(0, 0, size.width(), size.height()), - qt.Qt.TextExpandTabs, - text) - if (devicePixelRatio != 1.0 and - not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4 - _logger.error('devicePixelRatio not supported') - devicePixelRatio = 1.0 + # This does not provide the correct text bbox on macOS + # size = metrics.size(qt.Qt.TextExpandTabs, text) + # bounds = metrics.boundingRect( + # qt.QRect(0, 0, size.width(), size.height()), + # qt.Qt.TextExpandTabs, + # text) # Add extra border and handle devicePixelRatio width = bounds.width() * devicePixelRatio + 2 # align line size to 32 bits to ease conversion to numpy array width = 4 * ((width + 3) // 4) image = qt.QImage(width, - bounds.height() * devicePixelRatio, + bounds.height() * devicePixelRatio + 2, qt.QImage.Format_RGB888) if (devicePixelRatio != 1.0 and hasattr(image, 'setDevicePixelRatio')): # Qt 5 -- cgit v1.2.3