diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 87399e4454..6eb3e3f94f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Release Notes ============= +2.1.2: 2024/10/23 +----------------- + +* Bug fixes + + * `silx.gui``: Fixed memory leak: Updated OpenGL text rendering to use Qt when possible + 2.1.1: 2024/08/13 ----------------- diff --git a/src/silx/_version.py b/src/silx/_version.py index 55cbc824d1..e09f54063e 100644 --- a/src/silx/_version.py +++ b/src/silx/_version.py @@ -69,7 +69,7 @@ MAJOR = 2 MINOR = 1 -MICRO = 1 +MICRO = 2 RELEV = "final" # <16 SERIAL = 0 # <16 diff --git a/src/silx/gui/_glutils/font.py b/src/silx/gui/_glutils/font.py index 4c0268e30b..0da94255eb 100644 --- a/src/silx/gui/_glutils/font.py +++ b/src/silx/gui/_glutils/font.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2016-2023 European Synchrotron Radiation Facility +# 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 @@ -21,6 +21,7 @@ # THE SOFTWARE. # # ###########################################################################*/ +from __future__ import annotations """Text rasterisation feature leveraging Qt font and text layout support.""" __authors__ = ["T. Vincent"] @@ -28,12 +29,121 @@ __date__ = "13/10/2016" +import logging +import numpy + from .. import qt +from ..utils.image import convertQImageToArray +from ..utils.matplotlib import rasterMathText + -# Expose rasterMathText as part of this module -from ..utils.matplotlib import rasterMathText as rasterText # noqa +_logger = logging.getLogger(__name__) def getDefaultFontFamily() -> str: """Returns the default font family of the application""" return qt.QApplication.instance().font().family() + + +def rasterTextQt( + text: str, + font: qt.QFont, + dotsPerInch: float = 96.0, +) -> tuple[numpy.ndarray, float]: + """Raster text using Qt. + + 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 + """ + if not text: + _logger.info("Trying to raster empty text, replaced by white space") + text = " " # Replace empty text by white space to produce an image + + dotsPerMeter = int(dotsPerInch * 100 / 2.54) + + # get text size + image = qt.QImage(1, 1, qt.QImage.Format_Grayscale8) + image.setDotsPerMeterX(dotsPerMeter) + image.setDotsPerMeterY(dotsPerMeter) + + painter = qt.QPainter() + painter.begin(image) + painter.setPen(qt.Qt.white) + painter.setFont(font) + bounds = painter.boundingRect( + qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text + ) + painter.end() + + metrics = qt.QFontMetrics(font) + offset = metrics.ascent() / 72.0 * dotsPerInch + + # This does not provide the correct text bbox on macOS + # size = metrics.size(qt.Qt.TextExpandTabs, text) + # bounds = metrics.boundingRect( + # qt.QRect(0, 0, size.width(), size.height()), + # qt.Qt.TextExpandTabs, + # text) + + # Add extra border + width = bounds.width() + 2 + # align line size to 32 bits to ease conversion to numpy array + width = 4 * ((width + 3) // 4) + image = qt.QImage( + int(width), + int(bounds.height() + 2), + qt.QImage.Format_Grayscale8, + ) + image.setDotsPerMeterX(dotsPerMeter) + image.setDotsPerMeterY(dotsPerMeter) + image.fill(0) + + # Raster text + painter = qt.QPainter() + painter.begin(image) + painter.setPen(qt.Qt.white) + painter.setFont(font) + painter.drawText(bounds, qt.Qt.TextExpandTabs, text) + painter.end() + + array = convertQImageToArray(image) + + # Remove leading and trailing empty columns/rows but one on each side + filled_rows = numpy.nonzero(numpy.sum(array, axis=1))[0] + filled_columns = numpy.nonzero(numpy.sum(array, axis=0))[0] + + if len(filled_rows) == 0 or len(filled_columns) == 0: + return array, offset + return ( + numpy.ascontiguousarray( + array[ + 0 : filled_rows[-1] + 2, + max(0, filled_columns[0] - 1) : filled_columns[-1] + 2, + ] + ), + offset, + ) + + +def rasterText( + text: str, + font: qt.QFont, + dotsPerInch: float = 96.0, +) -> tuple[numpy.ndarray, float]: + """Raster text using Qt or matplotlib if there may be math syntax. + + It supports multiple lines. + + :param text: The text to raster + :param font: Font name or QFont to use + :param dotsPerInch: Created image resolution + :return: Corresponding image in gray scale and baseline offset from top + """ + + if text.count("$") >= 2: + return rasterMathText(text, font, dotsPerInch) + return rasterTextQt(text, font, dotsPerInch)