File qt6.patch of Package python-fabio
From 94f8076562f670101791d3ecb0fe76082bc7405e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Kieffer?=
<jerome.kieffer@terre-adelie.org>
Date: Wed, 12 Mar 2025 19:04:50 +0100
Subject: [PATCH 1/3] close #587
---
src/fabio/app/_matplotlib.py | 67 --------
src/fabio/app/_qt.py | 187 ---------------------
src/fabio/app/meson.build | 2 -
src/fabio/app/viewer.py | 6 +-
src/fabio/meson.build | 1 +
src/fabio/qt/__init__.py | 56 +++++++
src/fabio/qt/_pyqt6.py | 79 +++++++++
src/fabio/qt/_pyside_dynamic.py | 255 +++++++++++++++++++++++++++++
src/fabio/qt/_qt.py | 281 ++++++++++++++++++++++++++++++++
src/fabio/qt/_utils.py | 75 +++++++++
src/fabio/qt/inspect.py | 76 +++++++++
src/fabio/qt/matplotlib.py | 185 +++++++++++++++++++++
src/fabio/qt/meson.build | 12 ++
13 files changed, 1023 insertions(+), 259 deletions(-)
delete mode 100644 src/fabio/app/_matplotlib.py
delete mode 100644 src/fabio/app/_qt.py
create mode 100644 src/fabio/qt/__init__.py
create mode 100644 src/fabio/qt/_pyqt6.py
create mode 100644 src/fabio/qt/_pyside_dynamic.py
create mode 100644 src/fabio/qt/_qt.py
create mode 100644 src/fabio/qt/_utils.py
create mode 100644 src/fabio/qt/inspect.py
create mode 100644 src/fabio/qt/matplotlib.py
create mode 100644 src/fabio/qt/meson.build
Index: fabio-2024.9.0/src/fabio/app/_matplotlib.py
===================================================================
--- fabio-2024.9.0.orig/src/fabio/app/_matplotlib.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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 module inits matplotlib and setups the backend to use.
-
-It MUST be imported prior to any other import of matplotlib.
-
-It provides the matplotlib :class:`FigureCanvasQTAgg` class corresponding
-to the used backend.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/01/2018"
-
-
-import sys
-import logging
-
-
-_logger = logging.getLogger(__name__)
-
-if 'matplotlib' in sys.modules:
- _logger.warning(
- 'matplotlib already loaded, setting its backend may not work')
-
-
-from . import _qt as qt
-
-import matplotlib
-
-if qt.BINDING == 'PySide':
- matplotlib.rcParams['backend'] = 'Qt4Agg'
- matplotlib.rcParams['backend.qt4'] = 'PySide'
- from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg # noqa
- from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT # noqa
-
-elif qt.BINDING == 'PyQt4':
- matplotlib.rcParams['backend'] = 'Qt4Agg'
- from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg # noqa
- from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT # noqa
-
-elif qt.BINDING == 'PyQt5':
- matplotlib.rcParams['backend'] = 'Qt5Agg'
- from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # noqa
- from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT # noqa
Index: fabio-2024.9.0/src/fabio/app/_qt.py
===================================================================
--- fabio-2024.9.0.orig/src/fabio/app/_qt.py
+++ /dev/null
@@ -1,187 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-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.
-#
-# ###########################################################################*/
-"""Common wrapper over Python Qt bindings:
-
-- `PyQt5 <http://pyqt.sourceforge.net/Docs/PyQt5/>`_,
-- `PyQt4 <http://pyqt.sourceforge.net/Docs/PyQt4/>`_ or
-- `PySide <http://www.pyside.org>`_.
-
-If a Qt binding is already loaded, it will use it, otherwise the different
-Qt bindings are tried in this order: PyQt4, PySide, PyQt5.
-
-The name of the loaded Qt binding is stored in the BINDING variable.
-
-For an alternative solution providing a structured namespace,
-see `qtpy <https://pypi.python.org/pypi/QtPy/>`_ which
-provides the namespace of PyQt5 over PyQt4 and PySide.
-"""
-
-__authors__ = ["V.A. Sole - ESRF Data Analysis"]
-__license__ = "MIT"
-__date__ = "26/12/2020"
-
-import logging
-import sys
-import traceback
-
-
-_logger = logging.getLogger(__name__)
-
-
-BINDING = None
-"""The name of the Qt binding in use: 'PyQt5', 'PyQt4' or 'PySide'."""
-
-QtBinding = None # noqa
-"""The Qt binding module in use: PyQt5, PyQt4 or PySide."""
-
-HAS_SVG = False
-"""True if Qt provides support for Scalable Vector Graphics (QtSVG)."""
-
-HAS_OPENGL = False
-"""True if Qt provides support for OpenGL (QtOpenGL)."""
-
-# First check for an already loaded wrapper
-if 'PySide.QtCore' in sys.modules:
- BINDING = 'PySide'
-
-elif 'PyQt5.QtCore' in sys.modules:
- BINDING = 'PyQt5'
-
-elif 'PyQt4.QtCore' in sys.modules:
- BINDING = 'PyQt4'
-
-else: # Then try Qt bindings
- try:
- import PyQt4 # noqa
- except ImportError:
- try:
- import PySide # noqa
- except ImportError:
- try:
- import PyQt5 # noqa
- except ImportError:
- raise ImportError(
- 'No Qt wrapper found. Install PyQt4, PyQt5 or PySide.')
- else:
- BINDING = 'PyQt5'
- else:
- BINDING = 'PySide'
- else:
- BINDING = 'PyQt4'
-
-
-if BINDING == 'PyQt4':
- _logger.debug('Using PyQt4 bindings')
- import PyQt4 as QtBinding # noqa
-
- from PyQt4.QtCore import * # noqa
- from PyQt4.QtGui import * # noqa
-
- try:
- from PyQt4.QtOpenGL import * # noqa
- except ImportError:
- _logger.info("PyQt4.QtOpenGL not available")
- HAS_OPENGL = False
- else:
- HAS_OPENGL = True
-
- try:
- from PyQt4.QtSvg import * # noqa
- except ImportError:
- _logger.info("PyQt4.QtSvg not available")
- HAS_SVG = False
- else:
- HAS_SVG = True
-
- from PyQt4.uic import loadUi # noqa
-
- Signal = pyqtSignal
-
- Property = pyqtProperty
-
- Slot = pyqtSlot
-
-elif BINDING == 'PySide':
- _logger.debug('Using PySide bindings')
-
- import PySide as QtBinding # noqa
-
- from PySide.QtCore import * # noqa
- from PySide.QtGui import * # noqa
-
- try:
- from PySide.QtOpenGL import * # noqa
- except ImportError:
- _logger.info("PySide.QtOpenGL not available")
- HAS_OPENGL = False
- else:
- HAS_OPENGL = True
-
- try:
- from PySide.QtSvg import * # noqa
- except ImportError:
- _logger.info("PySide.QtSvg not available")
- HAS_SVG = False
- else:
- HAS_SVG = True
-
- pyqtSignal = Signal
-
-elif BINDING == 'PyQt5':
- _logger.debug('Using PyQt5 bindings')
-
- import PyQt5 as QtBinding # noqa
-
- from PyQt5.QtCore import * # noqa
- from PyQt5.QtGui import * # noqa
- from PyQt5.QtWidgets import * # noqa
- from PyQt5.QtPrintSupport import * # noqa
-
- try:
- from PyQt5.QtOpenGL import * # noqa
- except ImportError:
- _logger.info("PySide.QtOpenGL not available")
- HAS_OPENGL = False
- else:
- HAS_OPENGL = True
-
- try:
- from PyQt5.QtSvg import * # noqa
- except ImportError:
- _logger.info("PyQt5.QtSvg not available")
- HAS_SVG = False
- else:
- HAS_SVG = True
-
- from PyQt5.uic import loadUi # noqa
-
- Signal = pyqtSignal
-
- Property = pyqtProperty
-
- Slot = pyqtSlot
-
-else:
- raise ImportError('No Qt wrapper found. Install PyQt4, PyQt5 or PySide')
Index: fabio-2024.9.0/src/fabio/app/meson.build
===================================================================
--- fabio-2024.9.0.orig/src/fabio/app/meson.build
+++ fabio-2024.9.0/src/fabio/app/meson.build
@@ -1,8 +1,6 @@
py.install_sources(
[
'__init__.py',
- '_qt.py',
- '_matplotlib.py',
'convert.py',
'eiger2cbf.py',
'viewer.py',
Index: fabio-2024.9.0/src/fabio/app/viewer.py
===================================================================
--- fabio-2024.9.0.orig/src/fabio/app/viewer.py
+++ fabio-2024.9.0/src/fabio/app/viewer.py
@@ -44,9 +44,9 @@ import sys
import os
import time
-from . import _qt as qt
-from ._matplotlib import FigureCanvasQTAgg
-from ._matplotlib import NavigationToolbar2QT
+from .. import qt
+from ..qt.matplotlib import FigureCanvasQTAgg
+from ..qt.matplotlib import NavigationToolbar2QT
from matplotlib.figure import Figure
import numpy
Index: fabio-2024.9.0/src/fabio/meson.build
===================================================================
--- fabio-2024.9.0.orig/src/fabio/meson.build
+++ fabio-2024.9.0/src/fabio/meson.build
@@ -3,6 +3,7 @@ subdir('benchmark')
subdir('compression')
subdir('ext')
subdir('test')
+subdir('qt')
subdir('utils')
py.install_sources([
Index: fabio-2024.9.0/src/fabio/qt/__init__.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/__init__.py
@@ -0,0 +1,56 @@
+# /*##########################################################################
+#
+# Copyright (c) 2004-2021 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.
+#
+# ###########################################################################*/
+"""Common wrapper over Python Qt bindings:
+
+- `PyQt5 <http://pyqt.sourceforge.net/Docs/PyQt5/>`_
+- `PySide6 <https://pypi.org/project/PySide6/>`_
+- `PyQt6 <https://pypi.org/project/PyQt6/>`_
+
+If a Qt binding is already loaded, it will be used.
+If the `QT_API` environment variable is set to one of the supported Qt bindings
+(case insensitive), this binding is loaded if available, otherwise the
+different Qt bindings are tried in this order: PyQt5, PySide6, PyQt6.
+
+The name of the loaded Qt binding is stored in the BINDING variable.
+
+This module provides a flat namespace over Qt bindings by importing
+all symbols from **QtCore**, **QtGui**, **QtWidgets** and **QtPrintSupport**
+packages and if available from **QtOpenGL** and **QtSvg** packages.
+
+Example of using :mod:`silx.gui.qt` module:
+
+>>> from silx.gui import qt
+>>> app = qt.QApplication([])
+>>> widget = qt.QWidget()
+
+For an alternative solution providing a structured namespace,
+see `qtpy <https://pypi.org/project/QtPy/>`_.
+"""
+
+from ._qt import * # noqa
+
+if BINDING == "PySide6":
+ # Import loadUi wrapper
+ from ._pyside_dynamic import loadUi # noqa
+from ._utils import * # noqa
Index: fabio-2024.9.0/src/fabio/qt/_pyqt6.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/_pyqt6.py
@@ -0,0 +1,80 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2021 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.
+#
+# ###########################################################################*/
+"""PyQt6 backward compatibility patching"""
+
+__authors__ = ["Thomas VINCENT"]
+__license__ = "MIT"
+__date__ = "02/09/2021"
+
+import enum
+import logging
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import PyQt6.sip
+except ImportError:
+ pass
+else:
+ def patch_enums(*modules):
+ """Patch PyQt6 modules to provide backward compatibility of enum values
+
+ :param modules: Modules to patch (e.g., PyQt6.QtCore).
+ """
+ for module in modules:
+ for clsName in dir(module):
+ cls = getattr(module, clsName, None)
+ if not isinstance(cls, PyQt6.sip.wrappertype) or not clsName.startswith(
+ "Q"
+ ):
+ continue
+
+ for qenumName in dir(cls):
+ if not qenumName[0].isupper():
+ continue
+ # Special cases to avoid overrides and mimic PySide6
+ if clsName == "QColorSpace" and qenumName in (
+ "Primaries",
+ "TransferFunction",
+ ):
+ continue
+ if qenumName in ("DeviceType", "PointerType"):
+ continue
+
+ qenum = getattr(cls, qenumName)
+ if not isinstance(qenum, enum.EnumMeta):
+ continue
+
+ if any(
+ map(
+ lambda ancestor: isinstance(ancestor, PyQt6.sip.wrappertype)
+ and qenum is getattr(ancestor, qenumName, None),
+ cls.__mro__[1:],
+ )
+ ):
+ continue # Only handle it once in case of inheritance
+
+ for name, value in qenum.__members__.items():
+ setattr(cls, name, value)
Index: fabio-2024.9.0/src/fabio/qt/_pyside_dynamic.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/_pyside_dynamic.py
@@ -0,0 +1,255 @@
+# Adapted from https://github.com/spyder-ide/qtpy/blob/296dee3da8aba381b3cf17da34a6d17626e50357/qtpy/uic.py
+# In PySide, loadUi does not exist, so we define it using QUiLoader, and
+# then make sure we expose that function. This is adapted from qt-helpers
+# which was released under a 3-clause BSD license:
+# qt-helpers - a common front-end to various Qt modules
+#
+# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * 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.
+# * Neither the name of the Glue project 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 HOLDER 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.
+#
+# Which itself was based on the solution at
+#
+# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
+#
+# which was released under the MIT license:
+#
+# Copyright (c) 2011 Sebastian Wiesner <lunaryorn@gmail.com>
+# Modifications by Charl Botha <cpbotha@vxlabs.com>
+#
+# 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.
+
+"""How to load a user interface dynamically with PySide6"""
+
+import logging
+_logger = logging.getLogger(__name__)
+from ._qt import BINDING
+
+if BINDING == "PySide6":
+
+ from PySide6.QtCore import QMetaObject, Property, Qt
+ from PySide6.QtWidgets import QFrame
+ from PySide6.QtUiTools import QUiLoader
+
+ # Specific custom widgets
+ class _Line(QFrame):
+ """Widget to use as 'Line' Qt designer"""
+
+ def __init__(self, parent=None):
+ super(_Line, self).__init__(parent)
+ self.setFrameShape(QFrame.HLine)
+ self.setFrameShadow(QFrame.Sunken)
+
+ def getOrientation(self):
+ shape = self.frameShape()
+ if shape == QFrame.HLine:
+ return Qt.Horizontal
+ elif shape == QFrame.VLine:
+ return Qt.Vertical
+ else:
+ raise RuntimeError("Wrong shape: %d", shape)
+
+ def setOrientation(self, orientation):
+ if orientation == Qt.Horizontal:
+ self.setFrameShape(QFrame.HLine)
+ elif orientation == Qt.Vertical:
+ self.setFrameShape(QFrame.VLine)
+ else:
+ raise ValueError("Unsupported orientation %s" % str(orientation))
+
+ orientation = Property("Qt::Orientation", getOrientation, setOrientation)
+
+
+ CUSTOM_WIDGETS = {"Line": _Line}
+ """Default custom widgets for `loadUi`"""
+
+ class UiLoader(QUiLoader):
+ """
+ Subclass of :class:`~PySide.QtUiTools.QUiLoader` to create the user
+ interface in a base instance.
+
+ Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does not
+ create a new instance of the top-level widget, but creates the user
+ interface in an existing instance of the top-level class if needed.
+
+ This mimics the behaviour of :func:`PyQt4.uic.loadUi`.
+ """
+
+ def __init__(self, baseinstance, customWidgets=None):
+ """
+ Create a loader for the given ``baseinstance``.
+
+ The user interface is created in ``baseinstance``, which must be an
+ instance of the top-level class in the user interface to load, or a
+ subclass thereof.
+
+ ``customWidgets`` is a dictionary mapping from class name to class
+ object for custom widgets. Usually, this should be done by calling
+ registerCustomWidget on the QUiLoader, but with PySide 1.1.2 on
+ Ubuntu 12.04 x86_64 this causes a segfault.
+
+ ``parent`` is the parent object of this loader.
+ """
+ QUiLoader.__init__(self, baseinstance)
+
+ self.baseinstance = baseinstance
+
+ if customWidgets is None:
+ self.customWidgets = {}
+ else:
+ self.customWidgets = customWidgets
+
+ def createWidget(self, class_name, parent=None, name=""):
+ """
+ Function that is called for each widget defined in ui file,
+ overridden here to populate baseinstance instead.
+ """
+
+ if parent is None and self.baseinstance:
+ # supposed to create the top-level widget, return the base
+ # instance instead
+ return self.baseinstance
+
+ else:
+ # For some reason, Line is not in the list of available
+ # widgets, but works fine, so we have to special case it here.
+ if class_name in self.availableWidgets() or class_name == "Line":
+ # create a new widget for child widgets
+ widget = QUiLoader.createWidget(self, class_name, parent, name)
+
+ else:
+ # If not in the list of availableWidgets, must be a custom
+ # widget. This will raise KeyError if the user has not
+ # supplied the relevant class_name in the dictionary or if
+ # customWidgets is empty.
+ try:
+ widget = self.customWidgets[class_name](parent)
+ except KeyError as error:
+ raise Exception(
+ f"No custom widget {class_name} " "found in customWidgets"
+ ) from error
+
+ if self.baseinstance:
+ # set an attribute for the new child widget on the base
+ # instance, just like PyQt4.uic.loadUi does.
+ setattr(self.baseinstance, name, widget)
+
+ return widget
+
+
+ def _get_custom_widgets(ui_file):
+ """
+ This function is used to parse a ui file and look for the <customwidgets>
+ section, then automatically load all the custom widget classes.
+ """
+
+ import sys
+ import importlib
+ from xml.etree.ElementTree import ElementTree
+
+ # Parse the UI file
+ etree = ElementTree()
+ ui = etree.parse(ui_file)
+
+ # Get the customwidgets section
+ custom_widgets = ui.find("customwidgets")
+
+ if custom_widgets is None:
+ return {}
+
+ custom_widget_classes = {}
+
+ for custom_widget in list(custom_widgets):
+ cw_class = custom_widget.find("class").text
+ cw_header = custom_widget.find("header").text
+
+ module = importlib.import_module(cw_header)
+
+ custom_widget_classes[cw_class] = getattr(module, cw_class)
+
+ return custom_widget_classes
+
+
+ def loadUi(uifile, baseinstance=None, package=None, resource_suffix=None):
+ """
+ Dynamically load a user interface from the given ``uifile``.
+
+ ``uifile`` is a string containing a file name of the UI file to load.
+
+ If ``baseinstance`` is ``None``, the a new instance of the top-level
+ widget will be created. Otherwise, the user interface is created within
+ the given ``baseinstance``. In this case ``baseinstance`` must be an
+ instance of the top-level widget class in the UI file to load, or a
+ subclass thereof. In other words, if you've created a ``QMainWindow``
+ interface in the designer, ``baseinstance`` must be a ``QMainWindow``
+ or a subclass thereof, too. You cannot load a ``QMainWindow`` UI file
+ with a plain :class:`~PySide.QtGui.QWidget` as ``baseinstance``.
+
+ :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on
+ the created user interface, so you can implemented your slots according
+ to its conventions in your widget class.
+
+ Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwise
+ return the newly created instance of the user interface.
+ """
+ if package is not None:
+ _logger.warning("loadUi package parameter not implemented with PySide")
+ if resource_suffix is not None:
+ _logger.warning("loadUi resource_suffix parameter not implemented with PySide")
+
+ # We parse the UI file and import any required custom widgets
+ customWidgets = _get_custom_widgets(uifile)
+
+ # Add CUSTOM_WIDGETS
+ for name, klass in CUSTOM_WIDGETS.items():
+ customWidgets.setdefault(name, klass)
+
+ loader = UiLoader(baseinstance, customWidgets)
+
+ widget = loader.load(uifile)
+ QMetaObject.connectSlotsByName(widget)
+ return widget
+else:
+ _logger.warning(f"Unsupported Qt binding: {BINDING}")
Index: fabio-2024.9.0/src/fabio/qt/_qt.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/_qt.py
@@ -0,0 +1,281 @@
+# /*##########################################################################
+#
+# Copyright (c) 2004-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
+# 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.
+#
+# ###########################################################################*/
+"""Load Qt binding"""
+
+__authors__ = ["V.A. Sole"]
+__license__ = "MIT"
+__date__ = "12/01/2022"
+
+
+import importlib
+import logging
+import os
+import sys
+import traceback
+
+from packaging.version import Version
+# from . import deprecation
+
+_logger = logging.getLogger(__name__)
+
+
+BINDING = None
+"""The name of the Qt binding in use: PyQt5, PySide6, PyQt6."""
+
+QtBinding = None # noqa
+"""The Qt binding module in use: PyQt5, PySide6, PyQt6."""
+
+HAS_SVG = False
+"""True if Qt provides support for Scalable Vector Graphics (QtSVG)."""
+
+HAS_OPENGL = False
+"""True if Qt provides support for OpenGL (QtOpenGL)."""
+
+
+def _select_binding() -> str:
+ """Select and load a Qt binding
+
+ Qt binding is selected according to:
+ - Already loaded binding
+ - QT_API environment variable
+ - Bindings order of priority
+
+ :raises ImportError:
+ :returns: Loaded binding
+ """
+ bindings = "PyQt5", "PySide6", "PyQt6"
+
+ envvar = os.environ.get("QT_API", "").lower()
+
+ # First check for an already loaded binding
+ for binding in bindings:
+ if f"{binding}.QtCore" in sys.modules:
+ if envvar and envvar != binding.lower():
+ _logger.warning(
+ f"Cannot satisfy QT_API={envvar} environment variable, {binding} is already loaded"
+ )
+ return binding
+
+ # Check if QT_API can be satisfied
+ if envvar:
+ selection = [b for b in bindings if envvar == b.lower()]
+ if not selection:
+ _logger.warning(f"Environment variable QT_API={envvar} is not supported")
+ else:
+ binding = selection[0]
+ try:
+ importlib.import_module(f"{binding}.QtCore")
+ except ImportError:
+ _logger.warning(
+ f"Cannot import {binding} specified by QT_API environment variable"
+ )
+ else:
+ return binding
+
+ # Try to load binding
+ for binding in bindings:
+ try:
+ importlib.import_module(f"{binding}.QtCore")
+ except ImportError:
+ if binding in sys.modules:
+ del sys.modules[binding]
+ else:
+ return binding
+
+ raise ImportError("No Qt wrapper found. Install PyQt5, PySide6, PyQt6.")
+
+
+BINDING = _select_binding()
+print(BINDING)
+
+
+if BINDING == "PyQt5":
+ _logger.debug("Using PyQt5 bindings")
+ from PyQt5 import QtCore
+
+ if sys.version_info >= (3, 10) and QtCore.PYQT_VERSION < 0x50E02:
+ raise RuntimeError(
+ "PyQt5 v%s is not supported, please upgrade it." % QtCore.PYQT_VERSION_STR
+ )
+
+ import PyQt5 as QtBinding # noqa
+
+ from PyQt5.QtCore import * # noqa
+ from PyQt5.QtGui import * # noqa
+ from PyQt5.QtWidgets import * # noqa
+ from PyQt5.QtPrintSupport import * # noqa
+
+ try:
+ from PyQt5.QtOpenGL import * # noqa
+ except ImportError:
+ _logger.info("PyQt5.QtOpenGL not available")
+ HAS_OPENGL = False
+ else:
+ HAS_OPENGL = True
+
+ try:
+ from PyQt5.QtSvg import * # noqa
+ except ImportError:
+ _logger.info("PyQt5.QtSvg not available")
+ HAS_SVG = False
+ else:
+ HAS_SVG = True
+
+ from PyQt5.uic import loadUi # noqa
+
+ Signal = pyqtSignal
+
+ Property = pyqtProperty
+
+ Slot = pyqtSlot
+
+ # Disable PyQt5's cooperative multi-inheritance since other bindings do not provide it.
+ # See https://www.riverbankcomputing.com/static/Docs/PyQt5/multiinheritance.html?highlight=inheritance
+ class _Foo(object):
+ pass
+
+ class QObject(QObject, _Foo):
+ pass
+
+elif BINDING == "PySide6":
+ _logger.debug("Using PySide6 bindings")
+
+ import PySide6 as QtBinding # noqa
+
+ if Version(QtBinding.__version__) < Version("6.4"):
+ raise RuntimeError(
+ f"PySide6 v{QtBinding.__version__} is not supported, please upgrade it."
+ )
+
+ from PySide6.QtCore import * # noqa
+ from PySide6.QtGui import * # noqa
+ from PySide6.QtWidgets import * # noqa
+ from PySide6.QtPrintSupport import * # noqa
+
+ try:
+ from PySide6.QtOpenGL import * # noqa
+ from PySide6.QtOpenGLWidgets import QOpenGLWidget # noqa
+ except ImportError:
+ _logger.info("PySide6's QtOpenGL or QtOpenGLWidgets not available")
+ HAS_OPENGL = False
+ else:
+ HAS_OPENGL = True
+
+ try:
+ from PySide6.QtSvg import * # noqa
+ except ImportError:
+ _logger.info("PySide6.QtSvg not available")
+ HAS_SVG = False
+ else:
+ HAS_SVG = True
+
+ pyqtSignal = Signal
+
+
+elif BINDING == "PyQt6":
+ _logger.debug("Using PyQt6 bindings")
+
+ # Monkey-patch module to expose enum values for compatibility
+ # All Qt modules loaded here should be patched.
+ from . import _pyqt6
+ from PyQt6 import QtCore
+
+ if QtCore.PYQT_VERSION < int("0x60300", 16):
+ raise RuntimeError(
+ "PyQt6 v%s is not supported, please upgrade it." % QtCore.PYQT_VERSION_STR
+ )
+
+ from PyQt6 import QtGui, QtWidgets, QtPrintSupport, QtOpenGL, QtSvg
+ from PyQt6 import QtTest as _QtTest
+
+ _pyqt6.patch_enums(
+ QtCore, QtGui, QtWidgets, QtPrintSupport, QtOpenGL, QtSvg, _QtTest
+ )
+
+ import PyQt6 as QtBinding # noqa
+
+ from PyQt6.QtCore import * # noqa
+ from PyQt6.QtGui import * # noqa
+ from PyQt6.QtWidgets import * # noqa
+ from PyQt6.QtPrintSupport import * # noqa
+
+ try:
+ from PyQt6.QtOpenGL import * # noqa
+ from PyQt6.QtOpenGLWidgets import QOpenGLWidget # noqa
+ except ImportError:
+ _logger.info("PyQt6's QtOpenGL or QtOpenGLWidgets not available")
+ HAS_OPENGL = False
+ else:
+ HAS_OPENGL = True
+
+ try:
+ from PyQt6.QtSvg import * # noqa
+ except ImportError:
+ _logger.info("PyQt6.QtSvg not available")
+ HAS_SVG = False
+ else:
+ HAS_SVG = True
+
+ from PyQt6.uic import loadUi # noqa
+
+ Signal = pyqtSignal
+
+ Property = pyqtProperty
+
+ Slot = pyqtSlot
+
+ # Disable PyQt6 cooperative multi-inheritance since other bindings do not provide it.
+ # See https://www.riverbankcomputing.com/static/Docs/PyQt6/multiinheritance.html?highlight=inheritance
+ class _Foo(object):
+ pass
+
+ class QObject(QObject, _Foo):
+ pass
+
+else:
+ raise ImportError("No Qt wrapper found. Install PyQt5, PySide6 or PyQt6")
+
+
+# provide a exception handler but not implement it by default
+def exceptionHandler(type_, value, trace):
+ """
+ This exception handler prevents quitting to the command line when there is
+ an unhandled exception while processing a Qt signal.
+
+ The script/application willing to use it should implement code similar to:
+
+ .. code-block:: python
+
+ if __name__ == "__main__":
+ sys.excepthook = qt.exceptionHandler
+
+ """
+ _logger.error("%s %s %s", type_, value, "".join(traceback.format_tb(trace)))
+ msg = QMessageBox()
+ msg.setWindowTitle("Unhandled exception")
+ msg.setIcon(QMessageBox.Critical)
+ msg.setInformativeText("%s %s\nPlease report details" % (type_, value))
+ msg.setDetailedText(("%s " % value) + "".join(traceback.format_tb(trace)))
+ msg.raise_()
+ msg.exec()
Index: fabio-2024.9.0/src/fabio/qt/_utils.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/_utils.py
@@ -0,0 +1,75 @@
+# /*##########################################################################
+#
+# Copyright (c) 2004-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
+# 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 module provides convenient functions related to Qt.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "30/11/2016"
+
+
+from . import _qt
+
+
+def getMouseEventPosition(event):
+ """Qt5/Qt6 compatibility wrapper to access QMouseEvent position
+
+ :param QMouseEvent event:
+ :returns: (x, y) as a tuple of float
+ """
+ if _qt.BINDING == "PyQt5":
+ return float(event.x()), float(event.y())
+ # Qt6
+ position = event.position()
+ return position.x(), position.y()
+
+
+def supportedImageFormats():
+ """Return a set of string of file format extensions supported by the
+ Qt runtime."""
+ formats = _qt.QImageReader.supportedImageFormats()
+ return set([str(data, "ascii") for data in formats])
+
+
+__globalThreadPoolInstance = None
+"""Store the own silx global thread pool"""
+
+
+def silxGlobalThreadPool():
+ """Manage an own QThreadPool to avoid issue on Qt5 Windows with the
+ default Qt global thread pool.
+
+ A thread pool is create in lazy loading. With a maximum of 4 threads.
+ Else `qt.Thread.idealThreadCount()` is used.
+
+ :rtype: qt.QThreadPool
+ """
+ global __globalThreadPoolInstance
+ if __globalThreadPoolInstance is None:
+ tp = _qt.QThreadPool()
+ # Setting maxThreadCount fixes a segfault with PyQt 5.9.1 on Windows
+ maxThreadCount = min(4, tp.maxThreadCount())
+ tp.setMaxThreadCount(maxThreadCount)
+ __globalThreadPoolInstance = tp
+ return __globalThreadPoolInstance
Index: fabio-2024.9.0/src/fabio/qt/inspect.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/inspect.py
@@ -0,0 +1,76 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2021 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 module provides functions to access Qt C++ object state:
+
+- :func:`isValid` to check whether a QObject C++ pointer is valid.
+- :func:`createdByPython` to check if a QObject was created from Python.
+- :func:`ownedByPython` to check if a QObject is currently owned by Python.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "08/10/2018"
+
+
+from . import _qt as qt
+
+
+if qt.BINDING == "PyQt5":
+ try:
+ from PyQt5.sip import isdeleted as _isdeleted # noqa
+ from PyQt5.sip import ispycreated as createdByPython # noqa
+ from PyQt5.sip import ispyowned as ownedByPython # noqa
+ except ImportError:
+ from sip import isdeleted as _isdeleted # noqa
+ from sip import ispycreated as createdByPython # noqa
+ from sip import ispyowned as ownedByPython # noqa
+
+ def isValid(obj):
+ """Returns True if underlying C++ object is valid.
+
+ :param QObject obj:
+ :rtype: bool
+ """
+ return not _isdeleted(obj)
+
+elif qt.BINDING == "PySide6":
+ from shiboken6 import isValid, createdByPython, ownedByPython # noqa
+
+elif qt.BINDING == "PyQt6":
+ from PyQt6.sip import isdeleted as _isdeleted # noqa
+ from PyQt6.sip import ispycreated as createdByPython # noqa
+ from PyQt6.sip import ispyowned as ownedByPython # noqa
+
+ def isValid(obj):
+ """Returns True if underlying C++ object is valid.
+
+ :param QObject obj:
+ :rtype: bool
+ """
+ return not _isdeleted(obj)
+
+else:
+ raise ImportError("Unsupported Qt binding %s" % qt.BINDING)
+
+__all__ = ["isValid", "createdByPython", "ownedByPython"]
Index: fabio-2024.9.0/src/fabio/qt/matplotlib.py
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/matplotlib.py
@@ -0,0 +1,185 @@
+# /*##########################################################################
+#
+# Copyright (c) 2016-2024 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 module initializes matplotlib and sets-up the backend to use.
+
+It MUST be imported prior to any other import of matplotlib.
+
+It provides the matplotlib :class:`FigureCanvasQTAgg` class corresponding
+to the used backend.
+"""
+from __future__ import annotations
+
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "02/05/2018"
+
+
+import io
+import matplotlib
+import numpy
+
+from .. import qt
+
+# This must be performed before any import from matplotlib
+if qt.BINDING in ("PySide6", "PyQt6", "PyQt5"):
+ matplotlib.use("Qt5Agg", force=False)
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # noqa
+ from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT # noqa
+else:
+ raise ImportError("Unsupported Qt binding: %s" % qt.BINDING)
+
+
+from matplotlib.font_manager import FontProperties
+from matplotlib.mathtext import MathTextParser
+from matplotlib.ticker import ScalarFormatter as _ScalarFormatter
+from matplotlib import figure, font_manager
+from packaging.version import Version
+
+_MATPLOTLIB_VERSION = Version(matplotlib.__version__)
+
+
+class DefaultTickFormatter(_ScalarFormatter):
+ """Tick label formatter"""
+
+ def __init__(self):
+ super().__init__(useOffset=True, useMathText=True)
+ self.set_scientific(True)
+ self.create_dummy_axis()
+
+ if _MATPLOTLIB_VERSION < Version("3.1.0"):
+
+ def format_ticks(self, values):
+ self.set_locs(values)
+ return [self(value, i) for i, value in enumerate(values)]
+
+
+_FONT_STYLES = {
+ qt.QFont.StyleNormal: "normal",
+ qt.QFont.StyleItalic: "italic",
+ qt.QFont.StyleOblique: "oblique",
+}
+
+
+def qFontToFontProperties(font: qt.QFont):
+ """Convert a QFont to a matplotlib FontProperties"""
+ weightFactor = 10 if qt.BINDING == "PyQt5" else 1
+ families = [font.family(), font.defaultFamily()]
+ if _MATPLOTLIB_VERSION >= Version("3.6.0"):
+ # Prevent 'Font family not found' warnings
+ availableNames = font_manager.get_font_names()
+ families = [f for f in families if f in availableNames]
+ families.append(font_manager.fontManager.defaultFamily["ttf"])
+
+ if "Sans" in font.family():
+ families.insert(0, "sans-serif")
+
+ return FontProperties(
+ family=families,
+ style=_FONT_STYLES[font.style()],
+ weight=weightFactor * font.weight(),
+ size=font.pointSizeF(),
+ )
+
+
+def rasterMathText(
+ text: str,
+ font: qt.QFont,
+ dotsPerInch: float = 96.0,
+) -> tuple[numpy.ndarray, float]:
+ """Raster text using matplotlib supporting latex-like math syntax.
+
+ It supports multiple lines.
+
+ :param text: The text to raster
+ :param font: Font to use
+ :param dotsPerInch: The DPI resolution of the created image
+ :return: Corresponding image in gray scale and baseline offset from top
+ """
+ # Implementation adapted from:
+ # https://github.com/matplotlib/matplotlib/blob/d624571a19aec7c7d4a24123643288fc27db17e7/lib/matplotlib/mathtext.py#L264
+
+ stripped_text = text.strip("\n")
+ font_prop = qFontToFontProperties(font)
+
+ parser = MathTextParser("path")
+ lines_info = [
+ parser.parse(line, prop=font_prop, dpi=dotsPerInch)
+ for line in stripped_text.split("\n")
+ ]
+ max_line_width = max(info[0] for info in lines_info)
+ # Use lp string as minimum height/ascent
+ ref_info = parser.parse("lp", prop=font_prop, dpi=dotsPerInch)
+ line_height = max(
+ ref_info[1],
+ *(info[1] for info in lines_info),
+ )
+ first_line_ascent = max(
+ ref_info[1] - ref_info[2], lines_info[0][1] - lines_info[0][2]
+ )
+
+ linespacing = 1.2
+
+ figure_height = numpy.ceil(line_height * len(lines_info) * linespacing) + 2
+ fig = figure.Figure(
+ figsize=(
+ (max_line_width + 1) / dotsPerInch,
+ figure_height / dotsPerInch,
+ )
+ )
+ fig.set_dpi(dotsPerInch)
+ text = fig.text(
+ 0,
+ 1,
+ stripped_text,
+ fontproperties=font_prop,
+ verticalalignment="top",
+ )
+ text.set_linespacing(linespacing)
+ with io.BytesIO() as buffer:
+ fig.savefig(buffer, dpi=dotsPerInch, format="raw")
+ canvas_width, canvas_height = fig.get_window_extent().max
+ buffer.seek(0)
+ image = numpy.frombuffer(buffer.read(), dtype=numpy.uint8).reshape(
+ int(canvas_height), int(canvas_width), 4
+ )
+
+ # RGB to inverted R channel
+ array = 255 - image[:, :, 0]
+
+ # Remove leading/trailing empty columns and trailing 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, first_line_ascent
+ return (
+ numpy.ascontiguousarray(
+ array[
+ 0 : filled_rows[-1] + 2,
+ max(0, filled_columns[0] - 1) : filled_columns[-1] + 2,
+ ]
+ ),
+ first_line_ascent,
+ )
Index: fabio-2024.9.0/src/fabio/qt/meson.build
===================================================================
--- /dev/null
+++ fabio-2024.9.0/src/fabio/qt/meson.build
@@ -0,0 +1,12 @@
+py.install_sources([
+ '__init__.py',
+ 'inspect.py',
+ 'matplotlib.py',
+ '_pyqt6.py',
+ '_pyside_dynamic.py',
+ '_qt.py',
+ '_utils.py',
+],
+ pure: false, # Will be installed next to binaries
+ subdir: 'fabio/qt' # Folder relative to site-packages to install to
+)