diff options
author | Picca Frédéric-Emmanuel <picca@debian.org> | 2024-02-05 16:30:07 +0100 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@debian.org> | 2024-02-05 16:30:07 +0100 |
commit | 04095a69f18767d222b16fae5b40f2b712cd6f7e (patch) | |
tree | d20abd3ee2f237319443e9dfd7500ad55d29a33d /src/silx/gui/_glutils | |
parent | 3427caf0e96690e56aac6231a91df8f0f7a64fc2 (diff) |
New upstream version 2.0.0+dfsg
Diffstat (limited to 'src/silx/gui/_glutils')
-rw-r--r-- | src/silx/gui/_glutils/Context.py | 2 | ||||
-rw-r--r-- | src/silx/gui/_glutils/FramebufferTexture.py | 68 | ||||
-rw-r--r-- | src/silx/gui/_glutils/OpenGLWidget.py | 142 | ||||
-rw-r--r-- | src/silx/gui/_glutils/Program.py | 33 | ||||
-rw-r--r-- | src/silx/gui/_glutils/Texture.py | 106 | ||||
-rw-r--r-- | src/silx/gui/_glutils/VertexBuffer.py | 82 | ||||
-rw-r--r-- | src/silx/gui/_glutils/__init__.py | 2 | ||||
-rw-r--r-- | src/silx/gui/_glutils/font.py | 158 | ||||
-rw-r--r-- | src/silx/gui/_glutils/gl.py | 60 | ||||
-rw-r--r-- | src/silx/gui/_glutils/test/__init__.py | 2 | ||||
-rw-r--r-- | src/silx/gui/_glutils/test/test_gl.py | 4 | ||||
-rw-r--r-- | src/silx/gui/_glutils/utils.py | 7 |
12 files changed, 292 insertions, 374 deletions
diff --git a/src/silx/gui/_glutils/Context.py b/src/silx/gui/_glutils/Context.py index d2ddaa3..c0def5c 100644 --- a/src/silx/gui/_glutils/Context.py +++ b/src/silx/gui/_glutils/Context.py @@ -36,8 +36,10 @@ import contextlib class _DEFAULT_CONTEXT(object): """The default value for OpenGL context""" + pass + _context = _DEFAULT_CONTEXT """The current OpenGL context""" diff --git a/src/silx/gui/_glutils/FramebufferTexture.py b/src/silx/gui/_glutils/FramebufferTexture.py index 75db264..6d1a8d9 100644 --- a/src/silx/gui/_glutils/FramebufferTexture.py +++ b/src/silx/gui/_glutils/FramebufferTexture.py @@ -53,13 +53,14 @@ class FramebufferTexture(object): _PACKED_FORMAT = gl.GL_DEPTH24_STENCIL8, gl.GL_DEPTH_STENCIL - def __init__(self, - internalFormat, - shape, - stencilFormat=gl.GL_DEPTH24_STENCIL8, - depthFormat=gl.GL_DEPTH24_STENCIL8, - **kwargs): - + def __init__( + self, + internalFormat, + shape, + stencilFormat=gl.GL_DEPTH24_STENCIL8, + depthFormat=gl.GL_DEPTH24_STENCIL8, + **kwargs, + ): self._texture = Texture(internalFormat, shape=shape, **kwargs) self._texture.prepare() @@ -69,24 +70,28 @@ class FramebufferTexture(object): with self: # Bind FBO # Attachments - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, - gl.GL_COLOR_ATTACHMENT0, - gl.GL_TEXTURE_2D, - self._texture.name, - 0) + 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) + 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 @@ -96,13 +101,15 @@ class FramebufferTexture(object): 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) + 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 @@ -110,7 +117,8 @@ class FramebufferTexture(object): if status != gl.GL_FRAMEBUFFER_COMPLETE: _logger.error( "OpenGL framebuffer initialization not complete, display may fail (error %d)", - status) + status, + ) @property def shape(self): @@ -130,8 +138,10 @@ class FramebufferTexture(object): if self._name is not None: return self._name else: - raise RuntimeError("No OpenGL framebuffer resource, \ - discard has already been called") + raise RuntimeError( + "No OpenGL framebuffer resource, \ + discard has already been called" + ) def bind(self): """Bind this framebuffer for rendering""" diff --git a/src/silx/gui/_glutils/OpenGLWidget.py b/src/silx/gui/_glutils/OpenGLWidget.py index d35bb73..59fa4f0 100644 --- a/src/silx/gui/_glutils/OpenGLWidget.py +++ b/src/silx/gui/_glutils/OpenGLWidget.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2023 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 @@ -43,16 +43,16 @@ from .._glutils import gl _logger = logging.getLogger(__name__) -if not hasattr(qt, 'QOpenGLWidget') and not hasattr(qt, 'QGLWidget'): +if not hasattr(qt, "QOpenGLWidget") and not hasattr(qt, "QGLWidget"): _OpenGLWidget = None else: - if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4 - _logger.info('Using QOpenGLWidget') + if hasattr(qt, "QOpenGLWidget"): # PyQt>=5.4 + _logger.info("Using QOpenGLWidget") _BaseOpenGLWidget = qt.QOpenGLWidget else: - _logger.info('Using QGLWidget') + _logger.info("Using QGLWidget") _BaseOpenGLWidget = qt.QGLWidget class _OpenGLWidget(_BaseOpenGLWidget): @@ -64,14 +64,17 @@ else: It provides the error reason as a str. """ - def __init__(self, parent, - alphaBufferSize=0, - depthBufferSize=24, - stencilBufferSize=8, - version=(2, 0), - f=qt.Qt.Widget): + def __init__( + self, + parent, + alphaBufferSize=0, + depthBufferSize=24, + stencilBufferSize=8, + version=(2, 0), + f=qt.Qt.Widget, + ): # True if using QGLWidget, False if using QOpenGLWidget - self.__legacy = not hasattr(qt, 'QOpenGLWidget') + self.__legacy = not hasattr(qt, "QOpenGLWidget") self.__devicePixelRatio = 1.0 self.__requestedOpenGLVersion = int(version[0]), int(version[1]) @@ -131,12 +134,23 @@ else: # 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) + 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 @@ -171,13 +185,13 @@ else: def initializeGL(self): parent = self.parent() if parent is None: - _logger.error('_OpenGLWidget has no parent') + _logger.error("_OpenGLWidget has no parent") return # Check OpenGL version if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion(): try: - gl.glGetError() # clear any previous error (if any) + gl.glGetError() # clear any previous error (if any) version = gl.glGetString(gl.GL_VERSION) except: version = None @@ -185,18 +199,19 @@ else: if version: self.__isValid = True else: - errMsg = 'OpenGL not available' - if sys.platform.startswith('linux'): - errMsg += ': If connected remotely, ' \ - 'GLX forwarding might be disabled.' + 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) + errMsg = "OpenGL %d.%d not available" % self.getRequestedOpenGLVersion() + _logger.error("OpenGL widget disabled: %s", errMsg) self.sigOpenGLContextError.emit(errMsg) self.__isValid = False @@ -206,7 +221,7 @@ else: def paintGL(self): parent = self.parent() if parent is None: - _logger.error('_OpenGLWidget has no parent') + _logger.error("_OpenGLWidget has no parent") return devicePixelRatio = self.window().windowHandle().devicePixelRatio() @@ -224,7 +239,7 @@ else: def resizeGL(self, width, height): parent = self.parent() if parent is None: - _logger.error('_OpenGLWidget has no parent') + _logger.error("_OpenGLWidget has no parent") return if self.isValid(): @@ -256,12 +271,15 @@ class OpenGLWidget(qt.QWidget): :param f: see :class:`QWidget` """ - def __init__(self, parent=None, - alphaBufferSize=0, - depthBufferSize=24, - stencilBufferSize=8, - version=(2, 0), - f=qt.Qt.Widget): + def __init__( + self, + parent=None, + alphaBufferSize=0, + depthBufferSize=24, + stencilBufferSize=8, + version=(2, 0), + f=qt.Qt.Widget, + ): super(OpenGLWidget, self).__init__(parent, f) layout = qt.QHBoxLayout(self) @@ -272,24 +290,26 @@ class OpenGLWidget(qt.QWidget): _check = isOpenGLAvailable(version=version, runtimeCheck=False) if _OpenGLWidget is None or not _check: - _logger.error('OpenGL-based widget disabled: %s', _check.error) + _logger.error("OpenGL-based widget disabled: %s", _check.error) self.__openGLWidget = None label = self._createErrorQLabel(_check.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) + return + + 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): @@ -297,7 +317,7 @@ class OpenGLWidget(qt.QWidget): :param str error: The error message to display""" label = qt.QLabel() - label.setText('OpenGL-based widget disabled:\n%s' % error) + label.setText("OpenGL-based widget disabled:\n%s" % error) label.setAlignment(qt.Qt.AlignCenter) label.setWordWrap(True) return label @@ -323,7 +343,7 @@ class OpenGLWidget(qt.QWidget): :rtype: float """ if self.__openGLWidget is None: - return 1. + return 1.0 else: return self.__openGLWidget.getDevicePixelRatio() @@ -333,13 +353,17 @@ class OpenGLWidget(qt.QWidget): :rtype: float """ screen = self.window().windowHandle().screen() - if screen is not None: - # TODO check if this is correct on different OS/screen - # OK on macOS10.12/qt5.13.2 - dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio() - else: # Fallback - dpi = 96. * self.getDevicePixelRatio() - return dpi + if screen is None: + return 96.0 * self.getDevicePixelRatio() + + physicalDPI = screen.physicalDotsPerInch() + if physicalDPI > 1000.0: + _logger.error( + "Reported screen DPI too high: %f, using default value instead", + physicalDPI, + ) + physicalDPI = 96.0 + return physicalDPI * self.getDevicePixelRatio() def getOpenGLVersion(self): """Returns the available OpenGL version. diff --git a/src/silx/gui/_glutils/Program.py b/src/silx/gui/_glutils/Program.py index d61c07d..b2adacf 100644 --- a/src/silx/gui/_glutils/Program.py +++ b/src/silx/gui/_glutils/Program.py @@ -55,8 +55,7 @@ class Program(object): array attached to it in order for the rendering to occur.... """ - def __init__(self, vertexShader, fragmentShader, - attrib0='position'): + def __init__(self, vertexShader, fragmentShader, attrib0="position"): self._vertexShader = vertexShader self._fragmentShader = fragmentShader self._attrib0 = attrib0 @@ -66,7 +65,7 @@ class Program(object): def _compileGL(vertexShader, fragmentShader, attrib0): program = gl.glCreateProgram() - gl.glBindAttribLocation(program, 0, attrib0.encode('ascii')) + gl.glBindAttribLocation(program, 0, attrib0.encode("ascii")) vertex = gl.glCreateShader(gl.GL_VERTEX_SHADER) gl.glShaderSource(vertex, vertexShader) @@ -79,8 +78,7 @@ class Program(object): fragment = gl.glCreateShader(gl.GL_FRAGMENT_SHADER) gl.glShaderSource(fragment, fragmentShader) gl.glCompileShader(fragment) - if gl.glGetShaderiv(fragment, - gl.GL_COMPILE_STATUS) != gl.GL_TRUE: + if gl.glGetShaderiv(fragment, gl.GL_COMPILE_STATUS) != gl.GL_TRUE: raise RuntimeError(gl.glGetShaderInfoLog(fragment)) gl.glAttachShader(program, fragment) gl.glDeleteShader(fragment) @@ -90,16 +88,15 @@ class Program(object): raise RuntimeError(gl.glGetProgramInfoLog(program)) attributes = {} - for index in range(gl.glGetProgramiv(program, - gl.GL_ACTIVE_ATTRIBUTES)): + for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_ATTRIBUTES)): name = gl.glGetActiveAttrib(program, index)[0] - namestr = name.decode('ascii') + namestr = name.decode("ascii") attributes[namestr] = gl.glGetAttribLocation(program, name) uniforms = {} for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_UNIFORMS)): name = gl.glGetActiveUniform(program, index)[0] - namestr = name.decode('ascii') + namestr = name.decode("ascii") uniforms[namestr] = gl.glGetUniformLocation(program, name) return program, attributes, uniforms @@ -107,8 +104,7 @@ class Program(object): def _getProgramInfo(self): glcontext = Context.getCurrent() if glcontext not in self._programs: - raise RuntimeError( - "Program was not compiled for current OpenGL context.") + raise RuntimeError("Program was not compiled for current OpenGL context.") return self._programs[glcontext] @property @@ -152,16 +148,15 @@ class Program(object): if glcontext not in self._programs: self._programs[glcontext] = self._compileGL( - self._vertexShader, - self._fragmentShader, - self._attrib0) + self._vertexShader, self._fragmentShader, self._attrib0 + ) if _logger.getEffectiveLevel() <= logging.DEBUG: gl.glValidateProgram(self.program) - if gl.glGetProgramiv( - self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE: - _logger.debug('Cannot validate program: %s', - gl.glGetProgramInfoLog(self.program)) + if gl.glGetProgramiv(self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE: + _logger.debug( + "Cannot validate program: %s", gl.glGetProgramInfoLog(self.program) + ) gl.glUseProgram(self.program) @@ -198,4 +193,4 @@ class Program(object): gl.glUniformMatrix4fv(location, count, transpose, value) elif not safe: - raise KeyError('No uniform: %s' % name) + raise KeyError("No uniform: %s" % name) diff --git a/src/silx/gui/_glutils/Texture.py b/src/silx/gui/_glutils/Texture.py index 76bdcd8..aac380d 100644 --- a/src/silx/gui/_glutils/Texture.py +++ b/src/silx/gui/_glutils/Texture.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility +# Copyright (c) 2014-2023 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 @@ -28,11 +28,7 @@ __license__ = "MIT" __date__ = "04/10/2016" -try: - from collections import abc -except ImportError: # Python2 support - import collections as abc - +from collections import abc from ctypes import c_void_p import logging @@ -62,10 +58,17 @@ class Texture(object): :type wrap: OpenGL wrap mode or 2 or 3-tuple of wrap mode """ - def __init__(self, internalFormat, data=None, format_=None, - shape=None, texUnit=0, - minFilter=None, magFilter=None, wrap=None): - + def __init__( + self, + internalFormat, + data=None, + format_=None, + shape=None, + texUnit=0, + minFilter=None, + magFilter=None, + wrap=None, + ): self._internalFormat = internalFormat if format_ is None: format_ = self.internalFormat @@ -74,7 +77,7 @@ class Texture(object): assert shape is not None else: assert shape is None - data = numpy.array(data, copy=False, order='C') + data = numpy.array(data, copy=False, order="C") if format_ != gl.GL_RED: shape = data.shape[:-1] # Last dimension is channels else: @@ -164,9 +167,7 @@ class Texture(object): :rtype: bool """ - return (self._name is None or - self._texParameterUpdates or - self._deferredUpdates) + return self._name is None or self._texParameterUpdates or self._deferredUpdates def _prepareAndBind(self, texUnit=None): """Synchronizes the OpenGL texture""" @@ -200,10 +201,14 @@ class Texture(object): if offset is None: # Initialize texture if self.ndim == 2: _logger.debug( - 'Creating 2D texture shape: (%d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], - str(self.internalFormat), str(format_), str(type_)) + "Creating 2D texture shape: (%d, %d)," + " internal format: %s, format: %s, type: %s", + self.shape[0], + self.shape[1], + str(self.internalFormat), + str(format_), + str(type_), + ) gl.glTexImage2D( gl.GL_TEXTURE_2D, @@ -214,14 +219,20 @@ class Texture(object): 0, format_, type_, - data) + data, + ) else: _logger.debug( - 'Creating 3D texture shape: (%d, %d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], self.shape[2], - str(self.internalFormat), str(format_), str(type_)) + "Creating 3D texture shape: (%d, %d, %d)," + " internal format: %s, format: %s, type: %s", + self.shape[0], + self.shape[1], + self.shape[2], + str(self.internalFormat), + str(format_), + str(type_), + ) gl.glTexImage3D( gl.GL_TEXTURE_3D, @@ -233,32 +244,37 @@ class Texture(object): 0, format_, type_, - data) + data, + ) else: # Update already existing texture if self.ndim == 2: - gl.glTexSubImage2D(gl.GL_TEXTURE_2D, - 0, - offset[1], - offset[0], - data.shape[1], - data.shape[0], - format_, - type_, - data) + gl.glTexSubImage2D( + gl.GL_TEXTURE_2D, + 0, + offset[1], + offset[0], + data.shape[1], + data.shape[0], + format_, + type_, + data, + ) else: - gl.glTexSubImage3D(gl.GL_TEXTURE_3D, - 0, - offset[2], - offset[1], - offset[0], - data.shape[2], - data.shape[1], - data.shape[0], - format_, - type_, - data) + gl.glTexSubImage3D( + gl.GL_TEXTURE_3D, + 0, + offset[2], + offset[1], + offset[0], + data.shape[2], + data.shape[1], + data.shape[0], + format_, + type_, + data, + ) self._deferredUpdates = [] @@ -340,7 +356,7 @@ class Texture(object): :param bool copy: True (default) to copy data, False to use as is (do not modify) """ - data = numpy.array(data, copy=copy, order='C') + data = numpy.array(data, copy=copy, order="C") offset = tuple(offset) assert data.ndim == self.ndim diff --git a/src/silx/gui/_glutils/VertexBuffer.py b/src/silx/gui/_glutils/VertexBuffer.py index 65fff86..d71bbeb 100644 --- a/src/silx/gui/_glutils/VertexBuffer.py +++ b/src/silx/gui/_glutils/VertexBuffer.py @@ -50,15 +50,12 @@ class VertexBuffer(object): :param target: Target buffer: GL_ARRAY_BUFFER (default) or GL_ELEMENT_ARRAY_BUFFER """ + # OpenGL|ES 2.0 subset: _USAGES = gl.GL_STREAM_DRAW, gl.GL_STATIC_DRAW, gl.GL_DYNAMIC_DRAW _TARGETS = gl.GL_ARRAY_BUFFER, gl.GL_ELEMENT_ARRAY_BUFFER - def __init__(self, - data=None, - size=None, - usage=None, - target=None): + def __init__(self, data=None, size=None, usage=None, target=None): if usage is None: usage = gl.GL_STATIC_DRAW assert usage in self._USAGES @@ -76,20 +73,14 @@ class VertexBuffer(object): if data is None: assert size is not None self._size = size - gl.glBufferData(self._target, - self._size, - c_void_p(0), - self._usage) + gl.glBufferData(self._target, self._size, c_void_p(0), self._usage) else: - data = numpy.array(data, copy=False, order='C') + data = numpy.array(data, copy=False, order="C") if size is not None: assert size <= data.nbytes self._size = size or data.nbytes - gl.glBufferData(self._target, - self._size, - data, - self._usage) + gl.glBufferData(self._target, self._size, data, self._usage) gl.glBindBuffer(self._target, 0) @@ -109,8 +100,10 @@ class VertexBuffer(object): if self._name is not None: return self._name else: - raise RuntimeError("No OpenGL buffer resource, \ - discard has already been called") + raise RuntimeError( + "No OpenGL buffer resource, \ + discard has already been called" + ) @property def size(self): @@ -118,8 +111,10 @@ class VertexBuffer(object): if self._size is not None: return self._size else: - raise RuntimeError("No OpenGL buffer resource, \ - discard has already been called") + raise RuntimeError( + "No OpenGL buffer resource, \ + discard has already been called" + ) def bind(self): """Bind the vertex buffer""" @@ -132,7 +127,7 @@ class VertexBuffer(object): :param int offset: Offset in bytes in the buffer where to put the data :param int size: If provided, size of data to copy """ - data = numpy.array(data, copy=False, order='C') + data = numpy.array(data, copy=False, order="C") if size is None: size = data.nbytes assert offset + size <= self.size @@ -172,14 +167,9 @@ class VertexBufferAttrib(object): _GL_TYPES = gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT, gl.GL_INT - def __init__(self, - vbo, - type_, - size, - dimension=1, - offset=0, - stride=0, - normalization=False): + def __init__( + self, vbo, type_, size, dimension=1, offset=0, stride=0, normalization=False + ): self.vbo = vbo assert type_ in self._GL_TYPES self.type_ = type_ @@ -201,21 +191,25 @@ class VertexBufferAttrib(object): """Call glVertexAttribPointer with objects information""" normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE with self.vbo: - gl.glVertexAttribPointer(attribute, - self.dimension, - self.type_, - normalization, - self.stride, - c_void_p(self.offset)) + gl.glVertexAttribPointer( + attribute, + self.dimension, + self.type_, + normalization, + self.stride, + c_void_p(self.offset), + ) def copy(self): - return VertexBufferAttrib(self.vbo, - self.type_, - self.size, - self.dimension, - self.offset, - self.stride, - self.normalization) + return VertexBufferAttrib( + self.vbo, + self.type_, + self.size, + self.dimension, + self.offset, + self.stride, + self.normalization, + ) def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): @@ -241,7 +235,7 @@ def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): suffix = (0,) * len(arrays) for data, pre, post in zip(arrays, prefix, suffix): - data = numpy.array(data, copy=False, order='C') + data = numpy.array(data, copy=False, order="C") shape = data.shape assert len(shape) <= 2 type_ = numpyToGLType(data.dtype) @@ -250,8 +244,7 @@ def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): sizeinbytes = size * dimension * sizeofGLType(type_) sizeinbytes = 4 * ((sizeinbytes + 3) >> 2) # 4 bytes alignment copyoffset = vbosize + pre * dimension * sizeofGLType(type_) - info.append((data, type_, size, dimension, - vbosize, sizeinbytes, copyoffset)) + info.append((data, type_, size, dimension, vbosize, sizeinbytes, copyoffset)) vbosize += sizeinbytes vbo = VertexBuffer(size=vbosize, usage=usage) @@ -260,6 +253,5 @@ def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): for data, type_, size, dimension, offset, sizeinbytes, copyoffset in info: copysize = data.shape[0] * dimension * sizeofGLType(type_) vbo.update(data, offset=copyoffset, size=copysize) - result.append( - VertexBufferAttrib(vbo, type_, size, dimension, offset, 0)) + result.append(VertexBufferAttrib(vbo, type_, size, dimension, offset, 0)) return result diff --git a/src/silx/gui/_glutils/__init__.py b/src/silx/gui/_glutils/__init__.py index a7a4bee..9526ba4 100644 --- a/src/silx/gui/_glutils/__init__.py +++ b/src/silx/gui/_glutils/__init__.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility +# Copyright (c) 2014-2022 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 diff --git a/src/silx/gui/_glutils/font.py b/src/silx/gui/_glutils/font.py index bee9745..4c0268e 100644 --- a/src/silx/gui/_glutils/font.py +++ b/src/silx/gui/_glutils/font.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2016-2022 European Synchrotron Radiation Facility +# Copyright (c) 2016-2023 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 @@ -28,162 +28,12 @@ __license__ = "MIT" __date__ = "13/10/2016" -import logging -import numpy - from .. import qt -from ..utils.image import convertQImageToArray - -try: - from ..utils.matplotlib import rasterMathText -except ImportError: - rasterMathText = None -_logger = logging.getLogger(__name__) +# Expose rasterMathText as part of this module +from ..utils.matplotlib import rasterMathText as rasterText # noqa -def getDefaultFontFamily(): +def getDefaultFontFamily() -> str: """Returns the default font family of the application""" return qt.QApplication.instance().font().family() - - -# Font weights -ULTRA_LIGHT = 0 -"""Lightest characters: Minimum font weight""" - -LIGHT = 25 -"""Light characters""" - -NORMAL = 50 -"""Normal characters""" - -SEMI_BOLD = 63 -"""Between normal and bold characters""" - -BOLD = 74 -"""Thicker characters""" - -BLACK = 87 -"""Really thick characters""" - -ULTRA_BLACK = 99 -"""Thickest characters: Maximum font weight""" - - -def rasterTextQt(text, font, size=-1, weight=-1, italic=False, devicePixelRatio=1.0): - """Raster text using Qt. - - It supports multiple lines. - - :param str text: The text to raster - :param font: Font name or QFont to use - :type font: str or :class:`QFont` - :param int size: - Font size in points - Used only if font is given as name. - :param int weight: - Font weight in [0, 99], see QFont.Weight. - Used only if font is given as name. - :param bool italic: - True for italic font (default: False). - Used only if font is given as name. - :param float devicePixelRatio: - The current ratio between device and device-independent pixel - (default: 1.0) - :return: Corresponding image in gray scale and baseline offset from top - :rtype: (HxW numpy.ndarray of uint8, int) - """ - if not text: - _logger.info("Trying to raster empty text, replaced by white space") - text = " " # Replace empty text by white space to produce an image - - 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) - - # 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( - int(width), int(bounds.height() * devicePixelRatio + 2), qt.QImage.Format_RGB888 - ) - image.setDevicePixelRatio(devicePixelRatio) - - # TODO if Qt5 use Format_Grayscale8 instead - image.fill(0) - - # Raster text - painter = qt.QPainter() - painter.begin(image) - painter.setPen(qt.Qt.white) - painter.setFont(font) - painter.drawText(bounds, qt.Qt.TextExpandTabs, text) - painter.end() - - array = convertQImageToArray(image) - - # RGB to R - array = numpy.ascontiguousarray(array[:, :, 0]) - - # Remove leading and trailing empty columns/rows but one on each side - filled_rows = numpy.nonzero(numpy.sum(array, axis=1))[0] - filled_columns = numpy.nonzero(numpy.sum(array, axis=0))[0] - if len(filled_rows) == 0 or len(filled_columns) == 0: - return array, metrics.ascent() - - min_row = max(0, filled_rows[0] - 1) - array = array[ - min_row : filled_rows[-1] + 2, - max(0, filled_columns[0] - 1) : filled_columns[-1] + 2, - ] - - return array, metrics.ascent() - min_row - - -def rasterText(text, font, size=-1, weight=-1, italic=False, devicePixelRatio=1.0): - """Raster text using Qt or matplotlib if there may be math syntax. - - It supports multiple lines. - - :param str text: The text to raster - :param font: Font name or QFont to use - :type font: str or :class:`QFont` - :param int size: - Font size in points - Used only if font is given as name. - :param int weight: - Font weight in [0, 99], see QFont.Weight. - Used only if font is given as name. - :param bool italic: - True for italic font (default: False). - Used only if font is given as name. - :param float devicePixelRatio: - The current ratio between device and device-independent pixel - (default: 1.0) - :return: Corresponding image in gray scale and baseline offset from top - :rtype: (HxW numpy.ndarray of uint8, int) - """ - if rasterMathText is not None and text.count("$") >= 2: - return rasterMathText(text, font, size, weight, italic, devicePixelRatio) - else: - return rasterTextQt(text, font, size, weight, italic, devicePixelRatio) diff --git a/src/silx/gui/_glutils/gl.py b/src/silx/gui/_glutils/gl.py index fb0a3fa..aff7617 100644 --- a/src/silx/gui/_glutils/gl.py +++ b/src/silx/gui/_glutils/gl.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility +# Copyright (c) 2014-2022 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 @@ -31,13 +31,19 @@ __date__ = "25/07/2016" from contextlib import contextmanager as _contextmanager from ctypes import c_uint import logging +import sys +from typing import Optional + +from packaging.version import Version + _logger = logging.getLogger(__name__) import OpenGL + # Set the following to true for debugging if _logger.getEffectiveLevel() <= logging.DEBUG: - _logger.debug('Enabling PyOpenGL debug flags') + _logger.debug("Enabling PyOpenGL debug flags") OpenGL.ERROR_LOGGING = True OpenGL.ERROR_CHECKING = True OpenGL.ERROR_ON_COPY = True @@ -46,8 +52,14 @@ else: OpenGL.ERROR_CHECKING = False OpenGL.ERROR_ON_COPY = False +if sys.version_info >= (3, 12) and Version(OpenGL.__version__) <= Version("3.1.7"): + # Python3.12 patch: see https://github.com/mcfletch/pyopengl/pull/100 + OpenGL.FormatHandler.by_name("ctypesparameter").check.append("_ctypes.CArgObject") + + import OpenGL.GL as _GL from OpenGL.GL import * # noqa +import OpenGL.platform # Extentions core in OpenGL 3 from OpenGL.GL.ARB import framebuffer_object as _FBO @@ -60,9 +72,22 @@ try: GLchar except NameError: from ctypes import c_char + GLchar = c_char +def getPlatform() -> Optional[str]: + """Returns the name of the PyOpenGL class handling the platform. + + E.g., GLXPlatform, EGLPlatform + """ + try: + platform = OpenGL.platform.PLATFORM + except AttributeError: + return None + return platform.__class__.__name__ + + def getVersion() -> tuple: """Returns the GL version as tuple of integers. @@ -74,7 +99,7 @@ def getVersion() -> tuple: if isinstance(desc, bytes): desc = desc.decode("ascii") version = desc.split(" ", 1)[0] - return tuple([int(i) for i in version.split('.')]) + return tuple([int(i) for i in version.split(".")]) except Exception as e: raise ValueError("GL version not properly formatted") from e @@ -90,21 +115,23 @@ def testGL() -> bool: _logger.error("OpenGL version >=2.1 required, running with %s" % version) return False - from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB - from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB + if major == 2: + from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB + from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB - if not glInitFramebufferObjectARB(): - _logger.error("OpenGL GL_ARB_framebuffer_object extension required!") - return False + if not glInitFramebufferObjectARB(): + _logger.error("OpenGL GL_ARB_framebuffer_object extension required!") + return False + + if not glInitTextureRgARB(): + _logger.error("OpenGL GL_ARB_texture_rg extension required!") + return False - if not glInitTextureRgARB(): - _logger.error("OpenGL GL_ARB_texture_rg extension required!") - return False return True # Additional setup -if hasattr(glget, 'addGLGetConstant'): +if hasattr(glget, "addGLGetConstant"): glget.addGLGetConstant(GL_FRAMEBUFFER_BINDING, (1,)) @@ -145,6 +172,7 @@ def disabled(capacity, disable=True): # Additional OpenGL wrapping + def glGetActiveAttrib(program, index): """Wrap PyOpenGL glGetActiveAttrib""" bufsize = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH) @@ -158,28 +186,28 @@ def glGetActiveAttrib(program, index): def glDeleteRenderbuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument + if not hasattr(buffers, "__len__"): # Support single int argument buffers = [buffers] length = len(buffers) _FBO.glDeleteRenderbuffers(length, (c_uint * length)(*buffers)) def glDeleteFramebuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument + if not hasattr(buffers, "__len__"): # Support single int argument buffers = [buffers] length = len(buffers) _FBO.glDeleteFramebuffers(length, (c_uint * length)(*buffers)) def glDeleteBuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument + if not hasattr(buffers, "__len__"): # Support single int argument buffers = [buffers] length = len(buffers) _GL.glDeleteBuffers(length, (c_uint * length)(*buffers)) def glDeleteTextures(textures): - if not hasattr(textures, '__len__'): # Support single int argument + if not hasattr(textures, "__len__"): # Support single int argument textures = [textures] length = len(textures) _GL.glDeleteTextures((c_uint * length)(*textures)) diff --git a/src/silx/gui/_glutils/test/__init__.py b/src/silx/gui/_glutils/test/__init__.py index 5ad4c28..e9dd44d 100644 --- a/src/silx/gui/_glutils/test/__init__.py +++ b/src/silx/gui/_glutils/test/__init__.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2015-2022 European Synchrotron Radiation Facility +# Copyright (c) 2015-2019 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 diff --git a/src/silx/gui/_glutils/test/test_gl.py b/src/silx/gui/_glutils/test/test_gl.py index be9332b..d719c08 100644 --- a/src/silx/gui/_glutils/test/test_gl.py +++ b/src/silx/gui/_glutils/test/test_gl.py @@ -26,11 +26,11 @@ from .. import gl def test_version_bytes(mocker): - mocker.patch('silx.gui._glutils.gl.glGetString', return_value=b"3.0 Mock") + mocker.patch("silx.gui._glutils.gl.glGetString", return_value=b"3.0 Mock") assert gl.getVersion() == (3, 0) def test_version_str(mocker): """In case glGetString returns str""" - mocker.patch('silx.gui._glutils.gl.glGetString', return_value="3.0 Mock") + mocker.patch("silx.gui._glutils.gl.glGetString", return_value="3.0 Mock") assert gl.getVersion() == (3, 0) diff --git a/src/silx/gui/_glutils/utils.py b/src/silx/gui/_glutils/utils.py index 49b431a..56ac935 100644 --- a/src/silx/gui/_glutils/utils.py +++ b/src/silx/gui/_glutils/utils.py @@ -94,8 +94,9 @@ def segmentTrianglesIntersection(segment, triangles): subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1) subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2] intersect = numpy.logical_or( - numpy.all(subVolumes >= 0., axis=1), # All positive - numpy.all(subVolumes <= 0., axis=1)) # All negative + numpy.all(subVolumes >= 0.0, axis=1), # All positive + numpy.all(subVolumes <= 0.0, axis=1), + ) # All negative intersect = numpy.where(intersect)[0] # Indices of intersected triangles # Get barycentric coordinates @@ -112,7 +113,7 @@ def segmentTrianglesIntersection(segment, triangles): del volAlpha del volume - inSegmentMask = numpy.logical_and(t >= 0., t <= 1.) + inSegmentMask = numpy.logical_and(t >= 0.0, t <= 1.0) intersect = intersect[inSegmentMask] t = t[inSegmentMask] barycentric = barycentric[inSegmentMask] |