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
+)
openSUSE Build Service is sponsored by