From ce8ce0a56a4a0b0d0791041f95af27a6820fb297 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 11 Sep 2024 14:32:24 +1000 Subject: [PATCH] [feature] Support horizontal alignment in HTML labels For multi-line labels, this allows use of either: - HTML

attributes - CSS "text-align: xxx" - HTML

some text
tags Supported alignments are left, right, center and justify Horizontal alignment can be used in all contexts where HTML text is rendered, EXCEPT for curved labels (since they are restricted to single-line text) Sponsored by City of Freiburg im Breisgau --- .../core/auto_additions/qgstextblockformat.py | 17 +++ .../textrenderer/qgstextblock.sip.in | 18 +++ .../textrenderer/qgstextblockformat.sip.in | 123 ++++++++++++++++ python/PyQt6/core/core_auto.sip | 1 + .../core/auto_additions/qgstextblockformat.py | 17 +++ .../textrenderer/qgstextblock.sip.in | 18 +++ .../textrenderer/qgstextblockformat.sip.in | 123 ++++++++++++++++ python/core/core_auto.sip | 1 + src/core/CMakeLists.txt | 2 + src/core/textrenderer/qgstextblock.cpp | 5 + src/core/textrenderer/qgstextblock.h | 21 ++- src/core/textrenderer/qgstextblockformat.cpp | 63 +++++++++ src/core/textrenderer/qgstextblockformat.h | 131 ++++++++++++++++++ src/core/textrenderer/qgstextdocument.cpp | 7 +- src/core/textrenderer/qgstextrenderer.cpp | 15 +- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgstextblock.py | 9 ++ tests/src/python/test_qgstextblockformat.py | 49 +++++++ tests/src/python/test_qgstextdocument.py | 4 +- tests/src/python/test_qgstextrenderer.py | 33 +++++ .../html_align_rect_center_base.png | Bin 0 -> 16718 bytes .../html_align_rect_left_base.png | Bin 0 -> 16792 bytes .../html_align_rect_right_base.png | Bin 0 -> 16686 bytes 23 files changed, 650 insertions(+), 8 deletions(-) create mode 100644 python/PyQt6/core/auto_additions/qgstextblockformat.py create mode 100644 python/PyQt6/core/auto_generated/textrenderer/qgstextblockformat.sip.in create mode 100644 python/core/auto_additions/qgstextblockformat.py create mode 100644 python/core/auto_generated/textrenderer/qgstextblockformat.sip.in create mode 100644 src/core/textrenderer/qgstextblockformat.cpp create mode 100644 src/core/textrenderer/qgstextblockformat.h create mode 100644 tests/src/python/test_qgstextblockformat.py create mode 100644 tests/testdata/control_images/text_renderer/html_align_rect_center_base/html_align_rect_center_base.png create mode 100644 tests/testdata/control_images/text_renderer/html_align_rect_left_base/html_align_rect_left_base.png create mode 100644 tests/testdata/control_images/text_renderer/html_align_rect_right_base/html_align_rect_right_base.png diff --git a/python/PyQt6/core/auto_additions/qgstextblockformat.py b/python/PyQt6/core/auto_additions/qgstextblockformat.py new file mode 100644 index 000000000000..67d60ab39535 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgstextblockformat.py @@ -0,0 +1,17 @@ +# The following has been generated automatically from src/core/textrenderer/qgstextblockformat.h +# monkey patching scoped based enum +QgsTextBlockFormat.BooleanValue.NotSet.__doc__ = "Property is not set" +QgsTextBlockFormat.BooleanValue.SetTrue.__doc__ = "Property is set and ``True``" +QgsTextBlockFormat.BooleanValue.SetFalse.__doc__ = "Property is set and ``False``" +QgsTextBlockFormat.BooleanValue.__doc__ = """Status values for boolean format properties + +* ``NotSet``: Property is not set +* ``SetTrue``: Property is set and ``True`` +* ``SetFalse``: Property is set and ``False`` + +""" +# -- +try: + QgsTextBlockFormat.__group__ = ['textrenderer'] +except NameError: + pass diff --git a/python/PyQt6/core/auto_generated/textrenderer/qgstextblock.sip.in b/python/PyQt6/core/auto_generated/textrenderer/qgstextblock.sip.in index d42f48a0fd26..cdb9926d9e45 100644 --- a/python/PyQt6/core/auto_generated/textrenderer/qgstextblock.sip.in +++ b/python/PyQt6/core/auto_generated/textrenderer/qgstextblock.sip.in @@ -72,6 +72,24 @@ Returns ``True`` if the block is empty. int size() const; %Docstring Returns the number of fragments in the block. +%End + + const QgsTextBlockFormat &blockFormat() const; +%Docstring +Returns the block formatting for the fragment. + +.. seealso:: :py:func:`setBlockFormat` + +.. versionadded:: 3.40 +%End + + void setBlockFormat( const QgsTextBlockFormat &format ); +%Docstring +Sets the block ``format`` for the fragment. + +.. seealso:: :py:func:`blockFormat` + +.. versionadded:: 3.40 %End void applyCapitalization( Qgis::Capitalization capitalization ); diff --git a/python/PyQt6/core/auto_generated/textrenderer/qgstextblockformat.sip.in b/python/PyQt6/core/auto_generated/textrenderer/qgstextblockformat.sip.in new file mode 100644 index 000000000000..cc649266a738 --- /dev/null +++ b/python/PyQt6/core/auto_generated/textrenderer/qgstextblockformat.sip.in @@ -0,0 +1,123 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/textrenderer/qgstextblockformat.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsTextBlockFormat +{ +%Docstring(signature="appended") +Stores information relating to individual block formatting. + +These options encapsulate formatting options which override the default +settings from a :py:class:`QgsTextFormat` for individual text blocks. + +.. warning:: + + This API is not considered stable and may change in future QGIS versions. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgstextblockformat.h" +%End + public: + + QgsTextBlockFormat(); + + QgsTextBlockFormat( const QTextBlockFormat &format ); +%Docstring +Constructor for QgsTextBlockFormat, based on the specified QTextBlockFormat ``format``. +%End + + enum class BooleanValue + { + NotSet, + SetTrue, + SetFalse, + }; + + void overrideWith( const QgsTextBlockFormat &other ); +%Docstring +Override all the default/unset properties of the current block format +with the settings from another format. + +This will replace any default/unset existing settings with the +settings from ``other``. + +Any settings which are default/unset in ``other`` will be left unchanged. + +:param other: The format to override with. +%End + + bool hasHorizontalAlignmentSet() const; +%Docstring +Returns ``True`` if the format has an explicit horizontal alignment set. + +If ``False`` is returned then the horizontal alignment will be inherited. + +.. seealso:: :py:func:`setHasHorizontalAlignmentSet` + +.. seealso:: :py:func:`horizontalAlignment` +%End + + void setHasHorizontalAlignmentSet( bool set ); +%Docstring +Sets whether the format has an explicit horizontal alignment ``set``. + +If ``set`` is ``False`` then the horizontal alignment will be inherited. + +.. seealso:: :py:func:`hasHorizontalAlignmentSet` + +.. seealso:: :py:func:`setHorizontalAlignment` +%End + + Qgis::TextHorizontalAlignment horizontalAlignment() const; +%Docstring +Returns the format horizontal alignment. + +This property is only respected if :py:func:`~QgsTextBlockFormat.hasHorizontalAlignmentSet` is ``True``. + +.. seealso:: :py:func:`hasHorizontalAlignmentSet` + +.. seealso:: :py:func:`setHorizontalAlignment` +%End + + void setHorizontalAlignment( Qgis::TextHorizontalAlignment alignment ); +%Docstring +Sets the format horizontal ``alignment``. + +This property is only respected if :py:func:`~QgsTextBlockFormat.hasHorizontalAlignmentSet` is ``True``. + +.. seealso:: :py:func:`hasHorizontalAlignmentSet` + +.. seealso:: :py:func:`horizontalAlignment` +%End + + void updateFontForFormat( QFont &font, const QgsRenderContext &context, double scaleFactor = 1.0 ) const; +%Docstring +Updates the specified ``font`` in place, applying block formatting options which +are applicable on a font level when rendered in the given ``context``. + +The optional ``scaleFactor`` parameter can specify a font size scaling factor. It is recommended to set this to +:py:func:`QgsTextRenderer.calculateScaleFactorForFormat()` and then manually calculations +based on the resultant font metrics. Failure to do so will result in poor quality text rendering +at small font sizes. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/textrenderer/qgstextblockformat.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 853d4d954e9f..a7ba6b291f9b 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -726,6 +726,7 @@ %Include auto_generated/textrenderer/qgsfontmanager.sip %Include auto_generated/textrenderer/qgstextbackgroundsettings.sip %Include auto_generated/textrenderer/qgstextblock.sip +%Include auto_generated/textrenderer/qgstextblockformat.sip %Include auto_generated/textrenderer/qgstextbuffersettings.sip %Include auto_generated/textrenderer/qgstextcharacterformat.sip %Include auto_generated/textrenderer/qgstextdocument.sip diff --git a/python/core/auto_additions/qgstextblockformat.py b/python/core/auto_additions/qgstextblockformat.py new file mode 100644 index 000000000000..67d60ab39535 --- /dev/null +++ b/python/core/auto_additions/qgstextblockformat.py @@ -0,0 +1,17 @@ +# The following has been generated automatically from src/core/textrenderer/qgstextblockformat.h +# monkey patching scoped based enum +QgsTextBlockFormat.BooleanValue.NotSet.__doc__ = "Property is not set" +QgsTextBlockFormat.BooleanValue.SetTrue.__doc__ = "Property is set and ``True``" +QgsTextBlockFormat.BooleanValue.SetFalse.__doc__ = "Property is set and ``False``" +QgsTextBlockFormat.BooleanValue.__doc__ = """Status values for boolean format properties + +* ``NotSet``: Property is not set +* ``SetTrue``: Property is set and ``True`` +* ``SetFalse``: Property is set and ``False`` + +""" +# -- +try: + QgsTextBlockFormat.__group__ = ['textrenderer'] +except NameError: + pass diff --git a/python/core/auto_generated/textrenderer/qgstextblock.sip.in b/python/core/auto_generated/textrenderer/qgstextblock.sip.in index d43a3d343289..b8bb97b9f67e 100644 --- a/python/core/auto_generated/textrenderer/qgstextblock.sip.in +++ b/python/core/auto_generated/textrenderer/qgstextblock.sip.in @@ -72,6 +72,24 @@ Returns ``True`` if the block is empty. int size() const; %Docstring Returns the number of fragments in the block. +%End + + const QgsTextBlockFormat &blockFormat() const; +%Docstring +Returns the block formatting for the fragment. + +.. seealso:: :py:func:`setBlockFormat` + +.. versionadded:: 3.40 +%End + + void setBlockFormat( const QgsTextBlockFormat &format ); +%Docstring +Sets the block ``format`` for the fragment. + +.. seealso:: :py:func:`blockFormat` + +.. versionadded:: 3.40 %End void applyCapitalization( Qgis::Capitalization capitalization ); diff --git a/python/core/auto_generated/textrenderer/qgstextblockformat.sip.in b/python/core/auto_generated/textrenderer/qgstextblockformat.sip.in new file mode 100644 index 000000000000..cc649266a738 --- /dev/null +++ b/python/core/auto_generated/textrenderer/qgstextblockformat.sip.in @@ -0,0 +1,123 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/textrenderer/qgstextblockformat.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsTextBlockFormat +{ +%Docstring(signature="appended") +Stores information relating to individual block formatting. + +These options encapsulate formatting options which override the default +settings from a :py:class:`QgsTextFormat` for individual text blocks. + +.. warning:: + + This API is not considered stable and may change in future QGIS versions. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgstextblockformat.h" +%End + public: + + QgsTextBlockFormat(); + + QgsTextBlockFormat( const QTextBlockFormat &format ); +%Docstring +Constructor for QgsTextBlockFormat, based on the specified QTextBlockFormat ``format``. +%End + + enum class BooleanValue + { + NotSet, + SetTrue, + SetFalse, + }; + + void overrideWith( const QgsTextBlockFormat &other ); +%Docstring +Override all the default/unset properties of the current block format +with the settings from another format. + +This will replace any default/unset existing settings with the +settings from ``other``. + +Any settings which are default/unset in ``other`` will be left unchanged. + +:param other: The format to override with. +%End + + bool hasHorizontalAlignmentSet() const; +%Docstring +Returns ``True`` if the format has an explicit horizontal alignment set. + +If ``False`` is returned then the horizontal alignment will be inherited. + +.. seealso:: :py:func:`setHasHorizontalAlignmentSet` + +.. seealso:: :py:func:`horizontalAlignment` +%End + + void setHasHorizontalAlignmentSet( bool set ); +%Docstring +Sets whether the format has an explicit horizontal alignment ``set``. + +If ``set`` is ``False`` then the horizontal alignment will be inherited. + +.. seealso:: :py:func:`hasHorizontalAlignmentSet` + +.. seealso:: :py:func:`setHorizontalAlignment` +%End + + Qgis::TextHorizontalAlignment horizontalAlignment() const; +%Docstring +Returns the format horizontal alignment. + +This property is only respected if :py:func:`~QgsTextBlockFormat.hasHorizontalAlignmentSet` is ``True``. + +.. seealso:: :py:func:`hasHorizontalAlignmentSet` + +.. seealso:: :py:func:`setHorizontalAlignment` +%End + + void setHorizontalAlignment( Qgis::TextHorizontalAlignment alignment ); +%Docstring +Sets the format horizontal ``alignment``. + +This property is only respected if :py:func:`~QgsTextBlockFormat.hasHorizontalAlignmentSet` is ``True``. + +.. seealso:: :py:func:`hasHorizontalAlignmentSet` + +.. seealso:: :py:func:`horizontalAlignment` +%End + + void updateFontForFormat( QFont &font, const QgsRenderContext &context, double scaleFactor = 1.0 ) const; +%Docstring +Updates the specified ``font`` in place, applying block formatting options which +are applicable on a font level when rendered in the given ``context``. + +The optional ``scaleFactor`` parameter can specify a font size scaling factor. It is recommended to set this to +:py:func:`QgsTextRenderer.calculateScaleFactorForFormat()` and then manually calculations +based on the resultant font metrics. Failure to do so will result in poor quality text rendering +at small font sizes. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/textrenderer/qgstextblockformat.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 853d4d954e9f..a7ba6b291f9b 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -726,6 +726,7 @@ %Include auto_generated/textrenderer/qgsfontmanager.sip %Include auto_generated/textrenderer/qgstextbackgroundsettings.sip %Include auto_generated/textrenderer/qgstextblock.sip +%Include auto_generated/textrenderer/qgstextblockformat.sip %Include auto_generated/textrenderer/qgstextbuffersettings.sip %Include auto_generated/textrenderer/qgstextcharacterformat.sip %Include auto_generated/textrenderer/qgstextdocument.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 506745ab98b2..49f414628430 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -359,6 +359,7 @@ set(QGIS_CORE_SRCS textrenderer/qgsfontmanager.cpp textrenderer/qgstextbackgroundsettings.cpp textrenderer/qgstextblock.cpp + textrenderer/qgstextblockformat.cpp textrenderer/qgstextbuffersettings.cpp textrenderer/qgstextcharacterformat.cpp textrenderer/qgstextdocument.cpp @@ -1981,6 +1982,7 @@ set(QGIS_CORE_HDRS textrenderer/qgsfontmanager.h textrenderer/qgstextbackgroundsettings.h textrenderer/qgstextblock.h + textrenderer/qgstextblockformat.h textrenderer/qgstextbuffersettings.h textrenderer/qgstextcharacterformat.h textrenderer/qgstextdocument.h diff --git a/src/core/textrenderer/qgstextblock.cpp b/src/core/textrenderer/qgstextblock.cpp index fb90ca5ce191..b93f3c0af19a 100644 --- a/src/core/textrenderer/qgstextblock.cpp +++ b/src/core/textrenderer/qgstextblock.cpp @@ -86,6 +86,11 @@ int QgsTextBlock::size() const return mFragments.size(); } +void QgsTextBlock::setBlockFormat( const QgsTextBlockFormat &format ) +{ + mBlockFormat = format; +} + void QgsTextBlock::applyCapitalization( Qgis::Capitalization capitalization ) { for ( QgsTextFragment &fragment : mFragments ) diff --git a/src/core/textrenderer/qgstextblock.h b/src/core/textrenderer/qgstextblock.h index 17cf46f2cbf7..9c5501105c22 100644 --- a/src/core/textrenderer/qgstextblock.h +++ b/src/core/textrenderer/qgstextblock.h @@ -19,6 +19,7 @@ #include "qgis_sip.h" #include "qgis_core.h" #include "qgstextfragment.h" +#include "qgstextblockformat.h" #include "qgsstringutils.h" #include @@ -89,6 +90,24 @@ class CORE_EXPORT QgsTextBlock */ int size() const; + /** + * Returns the block formatting for the fragment. + * + * \see setBlockFormat() + * + * \since QGIS 3.40 + */ + const QgsTextBlockFormat &blockFormat() const { return mBlockFormat; } + + /** + * Sets the block \a format for the fragment. + * + * \see blockFormat() + * + * \since QGIS 3.40 + */ + void setBlockFormat( const QgsTextBlockFormat &format ); + /** * Applies a \a capitalization style to the block's text. * @@ -154,7 +173,7 @@ class CORE_EXPORT QgsTextBlock private: QVector< QgsTextFragment > mFragments; - + QgsTextBlockFormat mBlockFormat; }; #endif // QGSTEXTBLOCK_H diff --git a/src/core/textrenderer/qgstextblockformat.cpp b/src/core/textrenderer/qgstextblockformat.cpp new file mode 100644 index 000000000000..7b9192b01579 --- /dev/null +++ b/src/core/textrenderer/qgstextblockformat.cpp @@ -0,0 +1,63 @@ +/*************************************************************************** + qgstextblockformat.cpp + ----------------- + begin : September 2024 + copyright : (C) Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstextblockformat.h" +#include "qgsrendercontext.h" + +#include + + +Qgis::TextHorizontalAlignment convertTextBlockFormatAlign( const QTextBlockFormat &format, bool &set ) +{ + set = format.hasProperty( QTextFormat::BlockAlignment ); + if ( format.alignment() & Qt::AlignLeft ) + { + return Qgis::TextHorizontalAlignment::Left; + } + else if ( format.alignment() & Qt::AlignRight ) + { + return Qgis::TextHorizontalAlignment::Right; + } + else if ( format.alignment() & Qt::AlignHCenter ) + { + return Qgis::TextHorizontalAlignment::Center; + } + else if ( format.alignment() & Qt::AlignJustify ) + { + return Qgis::TextHorizontalAlignment::Justify; + } + + set = false; + return Qgis::TextHorizontalAlignment::Left; +} + +QgsTextBlockFormat::QgsTextBlockFormat( const QTextBlockFormat &format ) +{ + mHorizontalAlign = convertTextBlockFormatAlign( format, mHasHorizontalAlignSet ); +} + +void QgsTextBlockFormat::overrideWith( const QgsTextBlockFormat &other ) +{ + if ( mHasHorizontalAlignSet && other.hasHorizontalAlignmentSet() ) + { + mHorizontalAlign = other.mHorizontalAlign; + mHasHorizontalAlignSet = true; + } +} + +void QgsTextBlockFormat::updateFontForFormat( QFont &, const QgsRenderContext &, const double ) const +{ + +} diff --git a/src/core/textrenderer/qgstextblockformat.h b/src/core/textrenderer/qgstextblockformat.h new file mode 100644 index 000000000000..fb9bae6b9bc8 --- /dev/null +++ b/src/core/textrenderer/qgstextblockformat.h @@ -0,0 +1,131 @@ +/*************************************************************************** + qgstextblockformat.h + ----------------- + begin : September 2024 + copyright : (C) Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSTEXTBLOCKFORMAT_H +#define QGSTEXTBLOCKFORMAT_H + +#include "qgis_sip.h" +#include "qgis_core.h" +#include "qgis.h" + +#include +#include + +class QTextBlockFormat; +class QgsRenderContext; + +/** + * \class QgsTextBlockFormat + * \ingroup core + * \brief Stores information relating to individual block formatting. + * + * These options encapsulate formatting options which override the default + * settings from a QgsTextFormat for individual text blocks. + * + * \warning This API is not considered stable and may change in future QGIS versions. + * + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsTextBlockFormat +{ + public: + + QgsTextBlockFormat() = default; + + /** + * Constructor for QgsTextBlockFormat, based on the specified QTextBlockFormat \a format. + */ + QgsTextBlockFormat( const QTextBlockFormat &format ); + + //! Status values for boolean format properties + enum class BooleanValue + { + NotSet, //!< Property is not set + SetTrue, //!< Property is set and TRUE + SetFalse, //!< Property is set and FALSE + }; + + /** + * Override all the default/unset properties of the current block format + * with the settings from another format. + * + * This will replace any default/unset existing settings with the + * settings from \a other. + * + * Any settings which are default/unset in \a other will be left unchanged. + * + * \param other The format to override with. + */ + void overrideWith( const QgsTextBlockFormat &other ); + + /** + * Returns TRUE if the format has an explicit horizontal alignment set. + * + * If FALSE is returned then the horizontal alignment will be inherited. + * + * \see setHasHorizontalAlignmentSet() + * \see horizontalAlignment() + */ + bool hasHorizontalAlignmentSet() const { return mHasHorizontalAlignSet; } + + /** + * Sets whether the format has an explicit horizontal alignment \a set. + * + * If \a set is FALSE then the horizontal alignment will be inherited. + * + * \see hasHorizontalAlignmentSet() + * \see setHorizontalAlignment() + */ + void setHasHorizontalAlignmentSet( bool set ) { mHasHorizontalAlignSet = set; } + + /** + * Returns the format horizontal alignment. + * + * This property is only respected if hasHorizontalAlignmentSet() is TRUE. + * + * \see hasHorizontalAlignmentSet() + * \see setHorizontalAlignment() + */ + Qgis::TextHorizontalAlignment horizontalAlignment() const { return mHorizontalAlign; } + + /** + * Sets the format horizontal \a alignment. + * + * This property is only respected if hasHorizontalAlignmentSet() is TRUE. + * + * \see hasHorizontalAlignmentSet() + * \see horizontalAlignment() + */ + void setHorizontalAlignment( Qgis::TextHorizontalAlignment alignment ) { mHorizontalAlign = alignment; } + + /** + * Updates the specified \a font in place, applying block formatting options which + * are applicable on a font level when rendered in the given \a context. + * + * The optional \a scaleFactor parameter can specify a font size scaling factor. It is recommended to set this to + * QgsTextRenderer::calculateScaleFactorForFormat() and then manually calculations + * based on the resultant font metrics. Failure to do so will result in poor quality text rendering + * at small font sizes. + */ + void updateFontForFormat( QFont &font, const QgsRenderContext &context, double scaleFactor = 1.0 ) const; + + private: + + bool mHasHorizontalAlignSet = false; + Qgis::TextHorizontalAlignment mHorizontalAlign = Qgis::TextHorizontalAlignment::Left; + +}; + +#endif // QGSTEXTBLOCKFORMAT_H diff --git a/src/core/textrenderer/qgstextdocument.cpp b/src/core/textrenderer/qgstextdocument.cpp index 747b98fca975..e17f8a0f4289 100644 --- a/src/core/textrenderer/qgstextdocument.cpp +++ b/src/core/textrenderer/qgstextdocument.cpp @@ -117,6 +117,7 @@ QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) auto it = sourceBlock.begin(); QgsTextBlock block; + block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) ); while ( !it.atEnd() ) { const QTextFragment fragment = it.fragment(); @@ -189,6 +190,7 @@ QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines ) document.append( block ); block = QgsTextBlock(); + block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) ); } } else if ( fragmentText.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) ) @@ -296,6 +298,7 @@ void QgsTextDocument::splitLines( const QString &wrapCharacter, int autoWrapLeng for ( const QgsTextBlock &block : prevBlocks ) { QgsTextBlock destinationBlock; + destinationBlock.setBlockFormat( block.blockFormat() ); for ( const QgsTextFragment &fragment : block ) { QStringList thisParts; @@ -338,7 +341,9 @@ void QgsTextDocument::splitLines( const QString &wrapCharacter, int autoWrapLeng destinationBlock.clear(); for ( int i = 1 ; i < thisParts.size() - 1; ++i ) { - append( QgsTextBlock( QgsTextFragment( thisParts.at( i ), fragment.characterFormat() ) ) ); + QgsTextBlock partBlock( QgsTextFragment( thisParts.at( i ), fragment.characterFormat() ) ); + partBlock.setBlockFormat( block.blockFormat() ); + append( partBlock ); } destinationBlock.append( QgsTextFragment( thisParts.at( thisParts.size() - 1 ), fragment.characterFormat() ) ); } diff --git a/src/core/textrenderer/qgstextrenderer.cpp b/src/core/textrenderer/qgstextrenderer.cpp index 1cd3fbba1d30..ab05f28a7db8 100644 --- a/src/core/textrenderer/qgstextrenderer.cpp +++ b/src/core/textrenderer/qgstextrenderer.cpp @@ -1689,7 +1689,7 @@ void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace ); } -void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent drawType, Qgis::TextLayoutMode mode, const Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, Qgis::TextHorizontalAlignment hAlignment, +void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent drawType, Qgis::TextLayoutMode mode, const Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, const Qgis::TextHorizontalAlignment hAlignment, Qgis::TextVerticalAlignment vAlignment, double rotation ) { QPainter *maskPainter = context.maskPainter( context.currentMaskId() ); @@ -1714,8 +1714,6 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con double verticalAlignOffset = 0; - bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 ); - if ( mode == Qgis::TextLayoutMode::Rectangle && vAlignment != Qgis::TextVerticalAlignment::Top ) { const double overallHeight = documentSize.height(); @@ -1737,6 +1735,13 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con int blockIndex = 0; for ( const QgsTextBlock &block : document ) { + Qgis::TextHorizontalAlignment blockAlignment = hAlignment; + if ( block.blockFormat().hasHorizontalAlignmentSet() ) + blockAlignment = block.blockFormat().horizontalAlignment(); + const bool adjustForAlignment = blockAlignment != Qgis::TextHorizontalAlignment::Left && + ( mode != Qgis::TextLayoutMode::Labeling + || textLines.size() > 1 ); + const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 ) || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty(); @@ -1765,7 +1770,7 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con if ( adjustForAlignment ) { double labelWidthDiff = 0; - switch ( hAlignment ) + switch ( blockAlignment ) { case Qgis::TextHorizontalAlignment::Center: labelWidthDiff = ( labelWidest - blockWidth ) * 0.5; @@ -1798,7 +1803,7 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con case Qgis::TextLayoutMode::Point: { - switch ( hAlignment ) + switch ( blockAlignment ) { case Qgis::TextHorizontalAlignment::Right: xMultiLineOffset = labelWidthDiff - labelWidest; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 40516eb717d0..b71e9d463808 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -342,6 +342,7 @@ ADD_PYTHON_TEST(PyQgsTaskManager test_qgstaskmanager.py) ADD_PYTHON_TEST(PyQgsTemporalUtils test_qgstemporalutils.py) ADD_PYTHON_TEST(PyQgsTerrainProvider test_qgsterrainprovider.py) ADD_PYTHON_TEST(PyQgsTextBlock test_qgstextblock.py) +ADD_PYTHON_TEST(PyQgsTextBlockFormat test_qgstextblockformat.py) ADD_PYTHON_TEST(PyQgsTextCharacterFormat test_qgstextcharacterformat.py) ADD_PYTHON_TEST(PyQgsTextDocument test_qgstextdocument.py) ADD_PYTHON_TEST(PyQgsTextFragment test_qgstextfragment.py) diff --git a/tests/src/python/test_qgstextblock.py b/tests/src/python/test_qgstextblock.py index eead7548c6eb..20a547fe0703 100644 --- a/tests/src/python/test_qgstextblock.py +++ b/tests/src/python/test_qgstextblock.py @@ -17,6 +17,7 @@ QgsStringUtils, QgsTextBlock, QgsTextFragment, + QgsTextBlockFormat, QgsTextCharacterFormat ) @@ -40,6 +41,14 @@ def testConstructors(self): self.assertEqual(block[0].text(), fragment.text()) self.assertEqual(block.toPlainText(), 'ludicrous gibs!') + def test_format(self): + block = QgsTextBlock() + self.assertFalse(block.blockFormat().hasHorizontalAlignmentSet()) + format = QgsTextBlockFormat() + format.setHasHorizontalAlignmentSet(True) + block.setBlockFormat(format) + self.assertTrue(block.blockFormat().hasHorizontalAlignmentSet()) + def testFromPlainText(self): block = QgsTextBlock.fromPlainText('abc def') self.assertEqual(len(block), 1) diff --git a/tests/src/python/test_qgstextblockformat.py b/tests/src/python/test_qgstextblockformat.py new file mode 100644 index 000000000000..f7bd6b60b3dc --- /dev/null +++ b/tests/src/python/test_qgstextblockformat.py @@ -0,0 +1,49 @@ +"""QGIS Unit tests for QgsTextBlockFormat. + +Run with: ctest -V -R QgsTextBlockFormat + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +from qgis.PyQt.QtGui import QColor +from qgis.core import ( + Qgis, + QgsFontUtils, + QgsRenderContext, + QgsTextBlockFormat, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsTextBlockFormat(QgisTestCase): + + def setUp(self): + QgsFontUtils.loadStandardTestFonts(['Bold', 'Oblique']) + + def testGettersSetters(self): + format = QgsTextBlockFormat() + self.assertFalse(format.hasHorizontalAlignmentSet()) + self.assertEqual(format.horizontalAlignment(), Qgis.TextHorizontalAlignment.Left) + + format.setHasHorizontalAlignmentSet(True) + self.assertTrue(format.hasHorizontalAlignmentSet()) + format.setHorizontalAlignment(Qgis.TextHorizontalAlignment.Right) + self.assertEqual(format.horizontalAlignment(), Qgis.TextHorizontalAlignment.Right) + + def testUpdateFont(self): + context = QgsRenderContext() + font = QgsFontUtils.getStandardTestFont() + + format = QgsTextBlockFormat() + format.updateFontForFormat(font, context) + + # no effect for now... + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgstextdocument.py b/tests/src/python/test_qgstextdocument.py index 7aa20f28287e..2bd4497482e5 100644 --- a/tests/src/python/test_qgstextdocument.py +++ b/tests/src/python/test_qgstextdocument.py @@ -82,9 +82,11 @@ def testFromPlainTextWithTabs(self): self.assertEqual(doc[0][5].text(), 'd') def testFromHtml(self): - doc = QgsTextDocument.fromHtml(['abc
def ghi
jkl
', 'b c d', 'e']) + doc = QgsTextDocument.fromHtml(['abc
def ghi
jkl
', 'b c d', 'e']) self.assertEqual(len(doc), 5) self.assertEqual(len(doc[0]), 1) + self.assertTrue(doc[0].blockFormat().hasHorizontalAlignmentSet()) + self.assertEqual(doc[0].blockFormat().horizontalAlignment(), Qgis.TextHorizontalAlignment.Right) self.assertEqual(doc[0][0].text(), 'abc') self.assertEqual(doc[0][0].characterFormat().underline(), QgsTextCharacterFormat.BooleanValue.NotSet) self.assertEqual(doc[0][0].characterFormat().italic(), QgsTextCharacterFormat.BooleanValue.NotSet) diff --git a/tests/src/python/test_qgstextrenderer.py b/tests/src/python/test_qgstextrenderer.py index 1e248395c0d1..b1706f56a5dd 100644 --- a/tests/src/python/test_qgstextrenderer.py +++ b/tests/src/python/test_qgstextrenderer.py @@ -3968,6 +3968,39 @@ def testHtmlHeadingsLargerFont(self): '

h1

h2

h3

h4

h5
h6
'], point=QPointF(10, 350)) + def testHtmlAlignmentLeftBase(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.checkRender(format, 'html_align_rect_left_base', None, text=[ + '

Test some text

Short

test

test

center
'], + rect=QRectF(10, 10, 300, 300)) + + def testHtmlAlignmentRightBase(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.checkRender(format, 'html_align_rect_right_base', None, text=[ + '

Test some text

Short

test

test

center
'], + rect=QRectF(10, 10, 300, 300), alignment=Qgis.TextHorizontalAlignment.Right) + + def testHtmlAlignmentCenterBase(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.checkRender(format, 'html_align_rect_center_base', None, text=[ + '

Test some text

Short

test

test

center
'], + rect=QRectF(10, 10, 300, 300), alignment=Qgis.TextHorizontalAlignment.Center) + def testHtmlSuperSubscript(self): format = QgsTextFormat() format.setFont(getTestFont('bold')) diff --git a/tests/testdata/control_images/text_renderer/html_align_rect_center_base/html_align_rect_center_base.png b/tests/testdata/control_images/text_renderer/html_align_rect_center_base/html_align_rect_center_base.png new file mode 100644 index 0000000000000000000000000000000000000000..cd10b591688bfcdd7bdf8097e0dc3d2815cc441f GIT binary patch literal 16718 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 literal 0 HcmV?d00001