From c2782ffed81be3896360949b2a4fd0da5ac3f0ee Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 28 Jan 2025 10:25:34 +1000 Subject: [PATCH] Fix handling of multiple consecutive tabs with HTML text rendering Refs #60098 --- src/core/textrenderer/qgstextdocument.cpp | 9 +++++++-- tests/src/python/test_qgstextrenderer.py | 14 ++++++++++++++ .../text_tab_multiple_html.png | Bin 0 -> 5344 bytes .../text_tab_multiple_html_mask.png | Bin 0 -> 5167 bytes 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 tests/testdata/control_images/text_renderer/text_tab_multiple_html/text_tab_multiple_html.png create mode 100644 tests/testdata/control_images/text_renderer/text_tab_multiple_html/text_tab_multiple_html_mask.png diff --git a/src/core/textrenderer/qgstextdocument.cpp b/src/core/textrenderer/qgstextdocument.cpp index 69329a4a7676..c12bb93537c6 100644 --- a/src/core/textrenderer/qgstextdocument.cpp +++ b/src/core/textrenderer/qgstextdocument.cpp @@ -53,6 +53,10 @@ QgsTextDocument QgsTextDocument::fromPlainText( const QStringList &lines ) // a html or css tag doesn't mess things up. Instead, Qt will just silently // ignore html attributes it doesn't know about, like this replacement string #define TAB_REPLACEMENT_MARKER " ignore_me_i_am_a_tab " +// when splitting by the tab replacement marker we need to be tolerant to the +// spaces surrounding REPLACEMENT_MARKER being swallowed when multiple consecutive +// tab characters exist +#define TAB_REPLACEMENT_MARKER_RX " ?ignore_me_i_am_a_tab ?" QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) { @@ -73,6 +77,7 @@ QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) // by first replacing it with a string which QTextDocument won't mess with, and then // handle these markers as tab characters in the parsed HTML document. line.replace( QString( '\t' ), QStringLiteral( TAB_REPLACEMENT_MARKER ) ); + const thread_local QRegularExpression sTabReplacementMarkerRx( QStringLiteral( TAB_REPLACEMENT_MARKER_RX ) ); // cheat a little. Qt css requires some properties to have the "px" suffix. But we don't treat these properties // as pixels, because that doesn't scale well with different dpi render targets! So let's instead use just instead treat the suffix as @@ -162,7 +167,7 @@ QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) } splitFragment.setCharacterFormat( newFormat ); - const QStringList tabSplit = splitLine.split( QStringLiteral( TAB_REPLACEMENT_MARKER ) ); + const QStringList tabSplit = splitLine.split( sTabReplacementMarkerRx ); int index = 0; for ( const QString &part : tabSplit ) { @@ -210,7 +215,7 @@ QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) newFormat.overrideWith( blockFormat ); tmpFragment.setCharacterFormat( newFormat ); - const QStringList tabSplit = fragmentText.split( QStringLiteral( TAB_REPLACEMENT_MARKER ) ); + const QStringList tabSplit = fragmentText.split( sTabReplacementMarkerRx ); int index = 0; for ( const QString &part : tabSplit ) { diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index fa667ee5d647..f4607a4528db 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -3656,6 +3656,20 @@ def testDrawTabFixedSize(self): self.checkRender(format, "text_tab_fixed_size", text=["with\ttabs", "a\tb"]) ) + def testDrawTabsMultipleHtmlFixedSize(self): + format = QgsTextFormat() + format.setFont(getTestFont("bold")) + format.setSize(20) + format.setAllowHtmlFormatting(True) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setTabStopDistance(20) + format.setTabStopDistanceUnit(Qgis.RenderUnit.Millimeters) + self.assertTrue( + self.checkRender( + format, "text_tab_multiple_html", text=["with\t\ttabs", "a\t\tb"] + ) + ) + def testDrawTabPositionsFixedSize(self): format = QgsTextFormat() format.setFont(getTestFont("bold")) diff --git a/tests/testdata/control_images/text_renderer/text_tab_multiple_html/text_tab_multiple_html.png b/tests/testdata/control_images/text_renderer/text_tab_multiple_html/text_tab_multiple_html.png new file mode 100644 index 0000000000000000000000000000000000000000..db00646f40dddb04932e1f7b76c06d3732a538c0 GIT binary patch literal 5344 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