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 000000000000..98022f18add0 Binary files /dev/null and b/tests/testdata/control_images/labelingengine/expected_curved_html_wordspacing/expected_curved_html_wordspacing.png differ diff --git a/tests/testdata/control_images/text_renderer/html_word_spacing/html_word_spacing.png b/tests/testdata/control_images/text_renderer/html_word_spacing/html_word_spacing.png new file mode 100644 index 000000000000..5967d43b558c Binary files /dev/null and b/tests/testdata/control_images/text_renderer/html_word_spacing/html_word_spacing.png differ 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 000000000000..baf5abe35e90 Binary files /dev/null and b/tests/testdata/control_images/text_renderer/html_word_spacing_negative/html_word_spacing_negative.png differ