From 90524634f945abcfa0daaa31c1b23f3eefa420ba Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 10 Sep 2024 14:32:06 +1000 Subject: [PATCH] [feature] Add support for word-spacing CSS in html labels ...and other places HTML text formatting is accepted. This allows use of CSS "word-spacing: 12" to increase the word spacing in a section of HTML text. The word spacing is always treated as being in point units. --- .../qgstextcharacterformat.sip.in | 18 ++++++ .../qgstextcharacterformat.sip.in | 18 ++++++ .../textrenderer/qgstextcharacterformat.cpp | 18 ++++++ .../textrenderer/qgstextcharacterformat.h | 17 ++++++ src/core/textrenderer/qgstextdocument.cpp | 7 +++ tests/src/core/testqgslabelingengine.cpp | 57 ++++++++++++++++++ tests/src/python/test_qgstextrenderer.py | 35 +++++++++++ .../expected_curved_html_wordspacing.png | Bin 0 -> 13769 bytes .../html_word_spacing/html_word_spacing.png | Bin 0 -> 8796 bytes .../html_word_spacing_negative.png | Bin 0 -> 8452 bytes 10 files changed, 170 insertions(+) create mode 100644 tests/testdata/control_images/labelingengine/expected_curved_html_wordspacing/expected_curved_html_wordspacing.png create mode 100644 tests/testdata/control_images/text_renderer/html_word_spacing/html_word_spacing.png create mode 100644 tests/testdata/control_images/text_renderer/html_word_spacing_negative/html_word_spacing_negative.png diff --git a/python/PyQt6/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in b/python/PyQt6/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in index 6f2fc88ef776..0f70b3e1c3f4 100644 --- a/python/PyQt6/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in +++ b/python/PyQt6/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in @@ -140,6 +140,24 @@ and should be inherited. .. seealso:: :py:func:`fontWeight` .. versionadded:: 3.28 +%End + + double wordSpacing() const; +%Docstring +Returns the font word spacing, in points, or NaN if word spacing is not set and should be inherited. + +.. seealso:: :py:func:`setWordSpacing` + +.. versionadded:: 3.40 +%End + + void setWordSpacing( double spacing ); +%Docstring +Sets the font word ``spacing``, in points, or NaN if word spacing is not set and should be inherited. + +.. seealso:: :py:func:`wordSpacing` + +.. versionadded:: 3.40 %End BooleanValue italic() const; diff --git a/python/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in b/python/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in index 6f2fc88ef776..0f70b3e1c3f4 100644 --- a/python/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in +++ b/python/core/auto_generated/textrenderer/qgstextcharacterformat.sip.in @@ -140,6 +140,24 @@ and should be inherited. .. seealso:: :py:func:`fontWeight` .. versionadded:: 3.28 +%End + + double wordSpacing() const; +%Docstring +Returns the font word spacing, in points, or NaN if word spacing is not set and should be inherited. + +.. seealso:: :py:func:`setWordSpacing` + +.. versionadded:: 3.40 +%End + + void setWordSpacing( double spacing ); +%Docstring +Sets the font word ``spacing``, in points, or NaN if word spacing is not set and should be inherited. + +.. seealso:: :py:func:`wordSpacing` + +.. versionadded:: 3.40 %End BooleanValue italic() const; diff --git a/src/core/textrenderer/qgstextcharacterformat.cpp b/src/core/textrenderer/qgstextcharacterformat.cpp index 58e24bc664ea..08ef0944ffa4 100644 --- a/src/core/textrenderer/qgstextcharacterformat.cpp +++ b/src/core/textrenderer/qgstextcharacterformat.cpp @@ -48,6 +48,7 @@ QgsTextCharacterFormat::QgsTextCharacterFormat( const QTextCharFormat &format ) , mStyleName( format.font().styleName() ) , mItalic( format.hasProperty( QTextFormat::FontItalic ) ? ( format.fontItalic() ? BooleanValue::SetTrue : BooleanValue::SetFalse ) : BooleanValue::NotSet ) , mFontPointSize( format.hasProperty( QTextFormat::FontPointSize ) ? format.fontPointSize() : - 1 ) + , mWordSpacing( format.hasProperty( QTextFormat::FontWordSpacing ) ? format.fontWordSpacing() : std::numeric_limits< double >::quiet_NaN() ) , mStrikethrough( format.hasProperty( QTextFormat::FontStrikeOut ) ? ( format.fontStrikeOut() ? BooleanValue::SetTrue : BooleanValue::SetFalse ) : BooleanValue::NotSet ) , mUnderline( format.hasProperty( QTextFormat::FontUnderline ) ? ( format.fontUnderline() ? BooleanValue::SetTrue : BooleanValue::SetFalse ) : BooleanValue::NotSet ) , mOverline( format.hasProperty( QTextFormat::FontOverline ) ? ( format.fontOverline() ? BooleanValue::SetTrue : BooleanValue::SetFalse ) : BooleanValue::NotSet ) @@ -72,6 +73,8 @@ void QgsTextCharacterFormat::overrideWith( const QgsTextCharacterFormat &other ) mTextColor = other.mTextColor; if ( mFontPointSize == -1 && other.mFontPointSize != -1 ) mFontPointSize = other.mFontPointSize; + if ( std::isnan( mWordSpacing ) ) + mWordSpacing = other.mWordSpacing; if ( mFontFamily.isEmpty() && !other.mFontFamily.isEmpty() ) mFontFamily = other.mFontFamily; if ( mStrikethrough == BooleanValue::NotSet && other.mStrikethrough != BooleanValue::NotSet ) @@ -201,6 +204,11 @@ void QgsTextCharacterFormat::updateFontForFormat( QFont &font, const QgsRenderCo font.setOverline( mOverline == QgsTextCharacterFormat::BooleanValue::SetTrue ); if ( mStrikethrough != QgsTextCharacterFormat::BooleanValue::NotSet ) font.setStrikeOut( mStrikethrough == QgsTextCharacterFormat::BooleanValue::SetTrue ); + + if ( !std::isnan( mWordSpacing ) ) + { + font.setWordSpacing( scaleFactor * context.convertToPainterUnits( mWordSpacing, Qgis::RenderUnit::Points ) ); + } } QgsTextCharacterFormat::BooleanValue QgsTextCharacterFormat::italic() const @@ -222,3 +230,13 @@ void QgsTextCharacterFormat::setFontWeight( int fontWeight ) { mFontWeight = fontWeight; } + +double QgsTextCharacterFormat::wordSpacing() const +{ + return mWordSpacing; +} + +void QgsTextCharacterFormat::setWordSpacing( double spacing ) +{ + mWordSpacing = spacing; +} diff --git a/src/core/textrenderer/qgstextcharacterformat.h b/src/core/textrenderer/qgstextcharacterformat.h index 139201ff21b6..60addfabe2b4 100644 --- a/src/core/textrenderer/qgstextcharacterformat.h +++ b/src/core/textrenderer/qgstextcharacterformat.h @@ -149,6 +149,22 @@ class CORE_EXPORT QgsTextCharacterFormat */ void setFontWeight( int fontWeight ); + /** + * Returns the font word spacing, in points, or NaN if word spacing is not set and should be inherited. + * + * \see setWordSpacing() + * \since QGIS 3.40 + */ + double wordSpacing() const; + + /** + * Sets the font word \a spacing, in points, or NaN if word spacing is not set and should be inherited. + * + * \see wordSpacing() + * \since QGIS 3.40 + */ + void setWordSpacing( double spacing ); + /** * Returns whether the format has italic enabled. * @@ -274,6 +290,7 @@ class CORE_EXPORT QgsTextCharacterFormat BooleanValue mItalic = BooleanValue::NotSet; double mFontPointSize = -1; QString mFontFamily; + double mWordSpacing = std::numeric_limits< double >::quiet_NaN(); bool mHasVerticalAlignSet = false; Qgis::TextCharacterVerticalAlignment mVerticalAlign = Qgis::TextCharacterVerticalAlignment::Normal; diff --git a/src/core/textrenderer/qgstextdocument.cpp b/src/core/textrenderer/qgstextdocument.cpp index efd4c8b5cd94..6517442db8d6 100644 --- a/src/core/textrenderer/qgstextdocument.cpp +++ b/src/core/textrenderer/qgstextdocument.cpp @@ -73,6 +73,13 @@ QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) // handle these markers as tab characters in the parsed HTML document. line.replace( QString( '\t' ), QStringLiteral( TAB_REPLACEMENT_MARKER ) ); + // cheat a little. Qt css requires word-spacing to have the "px" suffix. But we don't treat word spacing + // as pixels, because that doesn't scale well with different dpi render targets! So let's instead use just instead treat the suffix as + // optional, and ignore ANY unit suffix the user has put, and then replace it with "px" so that Qt's css parsing engine can process it + // correctly... + const thread_local QRegularExpression sRxWordSpacingFix( QStringLiteral( "word-spacing:\\s*(-?\\d+(?:\\.\\d+)?)([a-zA-Z]*)" ) ); + line.replace( sRxWordSpacingFix, QStringLiteral( "word-spacing: \\1px" ) ); + sourceDoc.setHtml( line ); QTextBlock sourceBlock = sourceDoc.firstBlock(); diff --git a/tests/src/core/testqgslabelingengine.cpp b/tests/src/core/testqgslabelingengine.cpp index 35030030b7a8..c179e8899e21 100644 --- a/tests/src/core/testqgslabelingengine.cpp +++ b/tests/src/core/testqgslabelingengine.cpp @@ -74,6 +74,7 @@ class TestQgsLabelingEngine : public QgsTest void testCurvedLabelsHtmlFormatting(); void testCurvedPerimeterLabelsHtmlFormatting(); void testCurvedLabelsHtmlSuperSubscript(); + void testCurvedLabelsHtmlWordSpacing(); void testPointLabelTabs(); void testPointLabelTabsHtml(); void testPointLabelHtmlFormatting(); @@ -1955,6 +1956,62 @@ void TestQgsLabelingEngine::testCurvedLabelsHtmlSuperSubscript() QVERIFY( imageCheck( QStringLiteral( "label_curved_html_supersubscript" ), img, 20 ) ); } +void TestQgsLabelingEngine::testCurvedLabelsHtmlWordSpacing() +{ + // test line label rendering with HTML formatting + QgsPalLayerSettings settings; + setDefaultLabelParams( settings ); + + QgsTextFormat format = settings.format(); + format.setSize( 30 ); + format.setColor( QColor( 0, 0, 0 ) ); + format.setAllowHtmlFormatting( true ); + settings.setFormat( format ); + + settings.fieldName = QStringLiteral( "'test of wo space'" ); + settings.isExpression = true; + settings.placement = Qgis::LabelPlacement::Curved; + settings.labelPerPart = false; + settings.lineSettings().setLineAnchorPercent( 0.5 ); + settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::Strict ); + settings.lineSettings().setPlacementFlags( Qgis::LabelLinePlacementFlag::AboveLine | Qgis::LabelLinePlacementFlag::MapOrientation ); + settings.lineSettings().setAnchorTextPoint( QgsLabelLineSettings::AnchorTextPoint::CenterOfText ); + + std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + vl2->setRenderer( new QgsNullSymbolRenderer() ); + + QgsFeature f; + f.setAttributes( QgsAttributes() << 1 ); + f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000010, 190100 5000000, 190200 5000000)" ) ) ); + QVERIFY( vl2->dataProvider()->addFeature( f ) ); + + vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl2->setLabelsEnabled( true ); + + // make a fake render context + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setDestinationCrs( vl2->crs() ); + + mapSettings.setOutputSize( size ); + mapSettings.setExtent( f.geometry().boundingBox() ); + mapSettings.setLayers( QList() << vl2.get() ); + mapSettings.setOutputDpi( 96 ); + + QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings(); + engineSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false ); + engineSettings.setFlag( Qgis::LabelingFlag::DrawCandidates, true ); + mapSettings.setLabelingEngineSettings( engineSettings ); + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + QVERIFY( imageCheck( QStringLiteral( "curved_html_wordspacing" ), img, 20 ) ); +} + void TestQgsLabelingEngine::testCurvedLabelsHtmlFormatting() { // test line label rendering with HTML formatting diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index 87be77cf4bbc..55579b7ffbc2 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -4015,6 +4015,41 @@ def testHtmlSuperSubscriptBufferShadow(self): 'subNsup'], point=QPointF(50, 200)) + def testHtmlWordSpacing(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(30) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setColor(QColor(255, 0, 0)) + format.setAllowHtmlFormatting(True) + assert self.checkRenderPoint(format, 'html_word_spacing', None, text=[ + 'test of wo space'], + point=QPointF(10, 200)) + + def testHtmlWordSpacingPx(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(30) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setColor(QColor(255, 0, 0)) + format.setAllowHtmlFormatting(True) + # unit should be ignored, we always treat it as pt as pixels don't + # scale + assert self.checkRenderPoint(format, 'html_word_spacing', None, text=[ + 'test of wo space'], + point=QPointF(10, 200)) + + def testHtmlWordSpacingNegative(self): + format = QgsTextFormat() + format.setFont(getTestFont('bold')) + format.setSize(30) + format.setSizeUnit(QgsUnitTypes.RenderUnit.RenderPoints) + format.setColor(QColor(255, 0, 0)) + format.setAllowHtmlFormatting(True) + assert self.checkRenderPoint(format, 'html_word_spacing_negative', None, text=[ + 'test of wo space'], + point=QPointF(10, 200)) + def testTextRenderFormat(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) diff --git a/tests/testdata/control_images/labelingengine/expected_curved_html_wordspacing/expected_curved_html_wordspacing.png b/tests/testdata/control_images/labelingengine/expected_curved_html_wordspacing/expected_curved_html_wordspacing.png new file mode 100644 index 0000000000000000000000000000000000000000..98022f18add03e113e7cc7dffb36ad0e1b9646c6 GIT binary patch literal 13769 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 literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/text_renderer/html_word_spacing_negative/html_word_spacing_negative.png b/tests/testdata/control_images/text_renderer/html_word_spacing_negative/html_word_spacing_negative.png new file mode 100644 index 0000000000000000000000000000000000000000..baf5abe35e90800c52dae863c1d86a5e68c797b3 GIT binary patch literal 8452 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