[feature] Support CSS background property in labels/text renderer

This commit adds support for the CSS ``background-color`` and
``background-image`` properties when used with HTML labels.

- Backgrounds are supported for block items (eg ``<div>``) or inline
  items (eg ``<span>``)
- For images, the CSS should be formatted as ``background-image: url(xx)``.
  It supports local file paths, http links, or base64 embedded content
- Backgrounds are not supported for curved text
- HTML backgrounds are always rendered above any background shape for
  the label, and below drop shadows/buffers

Sponsored by City of Freiburg im Breisgau
This commit is contained in:
Nyall Dawson 2024-11-14 11:34:09 +10:00
parent 50601e5545
commit 6708501ee9
31 changed files with 508 additions and 7 deletions

View File

@ -198,6 +198,8 @@ Returns ``True`` if the block has a background set.
%Docstring
Returns the brush used for rendering the background of the block.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundBrush`
@ -209,8 +211,34 @@ Returns the brush used for rendering the background of the block.
%Docstring
Sets the ``brush`` used for rendering the background of the block.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`backgroundBrush`
.. versionadded:: 3.42
%End
QString backgroundImagePath() const;
%Docstring
Returns the path for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundImagePath`
.. versionadded:: 3.42
%End
void setBackgroundImagePath( const QString &path );
%Docstring
Sets the ``path`` for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`backgroundImagePath`
.. versionadded:: 3.42
%End

View File

@ -385,6 +385,8 @@ Returns ``True`` if the fragment has a background set.
%Docstring
Returns the brush used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundImagePath` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundBrush`
@ -396,8 +398,34 @@ Returns the brush used for rendering the background of the fragment.
%Docstring
Sets the ``brush`` used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundImagePath` set.
.. seealso:: :py:func:`backgroundBrush`
.. versionadded:: 3.42
%End
QString backgroundImagePath() const;
%Docstring
Returns the path for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundBrush` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundImagePath`
.. versionadded:: 3.42
%End
void setBackgroundImagePath( const QString &path );
%Docstring
Sets the ``path`` for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundBrush` set.
.. seealso:: :py:func:`backgroundImagePath`
.. versionadded:: 3.42
%End

View File

@ -172,6 +172,24 @@ to the fragment at the specified index within that block.
Returns the fixed height of the fragment at the specified block and fragment index, or -1 if the fragment does not have a fixed height.
.. versionadded:: 3.40
%End
double fragmentAscent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the ascent of the fragment at the specified block and fragment index.
.. seealso:: :py:func:`fragmentDescent`
.. versionadded:: 3.42
%End
double fragmentDescent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the descent of the fragment at the specified block and fragment index.
.. seealso:: :py:func:`fragmentAscent`
.. versionadded:: 3.42
%End
double verticalOrientationXOffset( int blockIndex ) const;
@ -187,6 +205,17 @@ Returns the maximum character width for the specified block.
double blockMaximumDescent( int blockIndex ) const;
%Docstring
Returns the maximum descent encountered in the specified block.
.. seealso:: :py:func:`blockMaximumAscent`
%End
double blockMaximumAscent( int blockIndex ) const;
%Docstring
Returns the maximum ascent encountered in the specified block.
.. seealso:: :py:func:`blockMaximumDescent`
.. versionadded:: 3.42
%End
QFont fragmentFont( int blockIndex, int fragmentIndex ) const;

View File

@ -198,6 +198,8 @@ Returns ``True`` if the block has a background set.
%Docstring
Returns the brush used for rendering the background of the block.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundBrush`
@ -209,8 +211,34 @@ Returns the brush used for rendering the background of the block.
%Docstring
Sets the ``brush`` used for rendering the background of the block.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`backgroundBrush`
.. versionadded:: 3.42
%End
QString backgroundImagePath() const;
%Docstring
Returns the path for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundImagePath`
.. versionadded:: 3.42
%End
void setBackgroundImagePath( const QString &path );
%Docstring
Sets the ``path`` for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextBlockFormat.backgroundBrush` set.
.. seealso:: :py:func:`backgroundImagePath`
.. versionadded:: 3.42
%End

View File

@ -385,6 +385,8 @@ Returns ``True`` if the fragment has a background set.
%Docstring
Returns the brush used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundImagePath` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundBrush`
@ -396,8 +398,34 @@ Returns the brush used for rendering the background of the fragment.
%Docstring
Sets the ``brush`` used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundImagePath` set.
.. seealso:: :py:func:`backgroundBrush`
.. versionadded:: 3.42
%End
QString backgroundImagePath() const;
%Docstring
Returns the path for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundBrush` set.
.. seealso:: :py:func:`hasBackground`
.. seealso:: :py:func:`setBackgroundImagePath`
.. versionadded:: 3.42
%End
void setBackgroundImagePath( const QString &path );
%Docstring
Sets the ``path`` for the image to be used for rendering the background of the fragment.
Alternatively, the format may have a :py:func:`~QgsTextCharacterFormat.backgroundBrush` set.
.. seealso:: :py:func:`backgroundImagePath`
.. versionadded:: 3.42
%End

View File

@ -172,6 +172,24 @@ to the fragment at the specified index within that block.
Returns the fixed height of the fragment at the specified block and fragment index, or -1 if the fragment does not have a fixed height.
.. versionadded:: 3.40
%End
double fragmentAscent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the ascent of the fragment at the specified block and fragment index.
.. seealso:: :py:func:`fragmentDescent`
.. versionadded:: 3.42
%End
double fragmentDescent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the descent of the fragment at the specified block and fragment index.
.. seealso:: :py:func:`fragmentAscent`
.. versionadded:: 3.42
%End
double verticalOrientationXOffset( int blockIndex ) const;
@ -187,6 +205,17 @@ Returns the maximum character width for the specified block.
double blockMaximumDescent( int blockIndex ) const;
%Docstring
Returns the maximum descent encountered in the specified block.
.. seealso:: :py:func:`blockMaximumAscent`
%End
double blockMaximumAscent( int blockIndex ) const;
%Docstring
Returns the maximum ascent encountered in the specified block.
.. seealso:: :py:func:`blockMaximumDescent`
.. versionadded:: 3.42
%End
QFont fragmentFont( int blockIndex, int fragmentIndex ) const;

View File

@ -45,6 +45,7 @@ Qgis::TextHorizontalAlignment convertTextBlockFormatAlign( const QTextBlockForma
QgsTextBlockFormat::QgsTextBlockFormat( const QTextBlockFormat &format )
: mBackgroundBrush( format.background() )
, mBackgroundPath( format.stringProperty( QTextFormat::BackgroundImageUrl ) )
, mLineHeight( format.hasProperty( QTextFormat::LineHeight ) && format.lineHeightType() != QTextBlockFormat::ProportionalHeight ? format.lineHeight() : std::numeric_limits< double >::quiet_NaN() )
, mLineHeightPercentage( format.hasProperty( QTextFormat::LineHeight ) && format.lineHeightType() == QTextBlockFormat::ProportionalHeight ? ( format.lineHeight() / 100.0 ) : std::numeric_limits< double >::quiet_NaN() )
{
@ -107,7 +108,7 @@ void QgsTextBlockFormat::updateFontForFormat( QFont &, const QgsRenderContext &,
bool QgsTextBlockFormat::hasBackground() const
{
return mBackgroundBrush.style() != Qt::NoBrush;
return mBackgroundBrush.style() != Qt::NoBrush || !mBackgroundPath.isEmpty();
}
QBrush QgsTextBlockFormat::backgroundBrush() const
@ -119,3 +120,13 @@ void QgsTextBlockFormat::setBackgroundBrush( const QBrush &brush )
{
mBackgroundBrush = brush;
}
QString QgsTextBlockFormat::backgroundImagePath() const
{
return mBackgroundPath;
}
void QgsTextBlockFormat::setBackgroundImagePath( const QString &path )
{
mBackgroundPath = path;
}

View File

@ -193,6 +193,8 @@ class CORE_EXPORT QgsTextBlockFormat
/**
* Returns the brush used for rendering the background of the block.
*
* Alternatively, the format may have a backgroundBrush() set.
*
* \see hasBackground()
* \see setBackgroundBrush()
* \since QGIS 3.42
@ -202,11 +204,34 @@ class CORE_EXPORT QgsTextBlockFormat
/**
* Sets the \a brush used for rendering the background of the block.
*
* Alternatively, the format may have a backgroundBrush() set.
*
* \see backgroundBrush()
* \since QGIS 3.42
*/
void setBackgroundBrush( const QBrush &brush );
/**
* Returns the path for the image to be used for rendering the background of the fragment.
*
* Alternatively, the format may have a backgroundBrush() set.
*
* \see hasBackground()
* \see setBackgroundImagePath()
* \since QGIS 3.42
*/
QString backgroundImagePath() const;
/**
* Sets the \a path for the image to be used for rendering the background of the fragment.
*
* Alternatively, the format may have a backgroundBrush() set.
*
* \see backgroundImagePath()
* \since QGIS 3.42
*/
void setBackgroundImagePath( const QString &path );
/**
* 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.
@ -221,6 +246,7 @@ class CORE_EXPORT QgsTextBlockFormat
private:
QBrush mBackgroundBrush;
QString mBackgroundPath;
double mLineHeight = std::numeric_limits< double >::quiet_NaN();
double mLineHeightPercentage = std::numeric_limits< double >::quiet_NaN();

View File

@ -53,6 +53,7 @@ QgsTextCharacterFormat::QgsTextCharacterFormat( const QTextCharFormat &format )
, 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 )
, mBackgroundBrush( format.background() )
, mBackgroundPath( format.background().style() == Qt::NoBrush ? format.stringProperty( QTextFormat::BackgroundImageUrl ) : QString() )
{
mVerticalAlign = convertTextCharFormatVAlign( format, mHasVerticalAlignSet );
@ -103,8 +104,10 @@ void QgsTextCharacterFormat::overrideWith( const QgsTextCharacterFormat &other )
mVerticalAlign = other.mVerticalAlign;
mHasVerticalAlignSet = true;
}
if ( mBackgroundBrush.style() == Qt::NoBrush && other.mBackgroundBrush.style() != Qt::NoBrush )
if ( mBackgroundBrush.style() == Qt::NoBrush && mBackgroundPath.isEmpty() && other.mBackgroundBrush.style() != Qt::NoBrush )
mBackgroundBrush = other.mBackgroundBrush;
if ( mBackgroundBrush.style() == Qt::NoBrush && mBackgroundPath.isEmpty() && !other.mBackgroundPath.isEmpty() )
mBackgroundPath = other.mBackgroundPath;
}
QColor QgsTextCharacterFormat::textColor() const
@ -255,6 +258,16 @@ void QgsTextCharacterFormat::updateFontForFormat( QFont &font, const QgsRenderCo
}
}
QString QgsTextCharacterFormat::backgroundImagePath() const
{
return mBackgroundPath;
}
void QgsTextCharacterFormat::setBackgroundImagePath( const QString &path )
{
mBackgroundPath = path;
}
QgsTextCharacterFormat::BooleanValue QgsTextCharacterFormat::italic() const
{
return mItalic;
@ -287,7 +300,7 @@ void QgsTextCharacterFormat::setWordSpacing( double spacing )
bool QgsTextCharacterFormat::hasBackground() const
{
return mBackgroundBrush.style() != Qt::NoBrush;
return mBackgroundBrush.style() != Qt::NoBrush || !mBackgroundPath.isEmpty();
}
QBrush QgsTextCharacterFormat::backgroundBrush() const

View File

@ -362,6 +362,8 @@ class CORE_EXPORT QgsTextCharacterFormat
/**
* Returns the brush used for rendering the background of the fragment.
*
* Alternatively, the format may have a backgroundImagePath() set.
*
* \see hasBackground()
* \see setBackgroundBrush()
* \since QGIS 3.42
@ -371,11 +373,34 @@ class CORE_EXPORT QgsTextCharacterFormat
/**
* Sets the \a brush used for rendering the background of the fragment.
*
* Alternatively, the format may have a backgroundImagePath() set.
*
* \see backgroundBrush()
* \since QGIS 3.42
*/
void setBackgroundBrush( const QBrush &brush );
/**
* Returns the path for the image to be used for rendering the background of the fragment.
*
* Alternatively, the format may have a backgroundBrush() set.
*
* \see hasBackground()
* \see setBackgroundImagePath()
* \since QGIS 3.42
*/
QString backgroundImagePath() const;
/**
* Sets the \a path for the image to be used for rendering the background of the fragment.
*
* Alternatively, the format may have a backgroundBrush() set.
*
* \see backgroundImagePath()
* \since QGIS 3.42
*/
void setBackgroundImagePath( const QString &path );
/**
* Updates the specified \a font in place, applying character formatting options which
* are applicable on a font level when rendered in the given \a context.
@ -409,6 +434,7 @@ class CORE_EXPORT QgsTextCharacterFormat
BooleanValue mOverline = BooleanValue::NotSet;
QBrush mBackgroundBrush;
QString mBackgroundPath;
};

View File

@ -91,6 +91,8 @@ struct BlockMetrics
QList< double > fragmentVerticalOffsets;
QList< double > fragmentFixedHeights;
QList< double > fragmentHorizontalAdvance;
QList< double > fragmentAscent;
QList< double > fragmentDescent;
QFont previousNonSuperSubScriptFont;
bool isFirstNonTabFragment = true;
@ -130,6 +132,8 @@ struct BlockMetrics
fragmentVerticalOffsets.clear();
fragmentFixedHeights.clear();
fragmentHorizontalAdvance.clear();
fragmentAscent.clear();
fragmentDescent.clear();
previousNonSuperSubScriptFont = QFont();
isFirstNonTabFragment = true;
}
@ -238,12 +242,15 @@ void QgsTextDocumentMetrics::finalizeBlock( QgsTextDocumentMetrics &res, const Q
res.mBaselineOffsetsCapHeightMode << documentMetrics.currentCapHeightBasedBaseline;
res.mBaselineOffsetsAscentBased << documentMetrics.currentAscentBasedBaseline;
res.mBlockMaxDescent << metrics.maxBlockDescent;
res.mBlockMaxAscent << metrics.maxBlockAscent;
res.mBlockMaxCharacterWidth << metrics.maxBlockMaxWidth;
res.mFragmentVerticalOffsetsLabelMode << metrics.fragmentVerticalOffsets;
res.mFragmentFixedHeights << metrics.fragmentFixedHeights;
res.mFragmentVerticalOffsetsRectMode << metrics.fragmentVerticalOffsets;
res.mFragmentVerticalOffsetsPointMode << metrics.fragmentVerticalOffsets;
res.mFragmentHorizontalAdvance << metrics.fragmentHorizontalAdvance;
res.mFragmentAscent << metrics.fragmentAscent;
res.mFragmentDescent << metrics.fragmentDescent;
res.mDocument.append( outputBlock );
outputBlock.clear();
@ -288,6 +295,8 @@ void QgsTextDocumentMetrics::processFragment( QgsTextDocumentMetrics &res, const
thisBlockMetrics.fragmentHorizontalAdvance << fragmentWidth;
thisBlockMetrics.fragmentFixedHeights << -1;
thisBlockMetrics.fragmentFonts << QFont();
thisBlockMetrics.fragmentAscent << 0;
thisBlockMetrics.fragmentDescent << 0;
currentOutputBlock.append( fragment );
}
else
@ -441,6 +450,8 @@ void QgsTextDocumentMetrics::processFragment( QgsTextDocumentMetrics &res, const
thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, imageHeight );
thisBlockMetrics.maxBlockCapHeight = std::max( thisBlockMetrics.maxBlockCapHeight, imageHeight );
thisBlockMetrics.fragmentAscent << imageHeight;
thisBlockMetrics.fragmentDescent << 0;
thisBlockMetrics.maxLineSpacing = std::max( thisBlockMetrics.maxLineSpacing, imageHeight + fm.leading() / scaleFactor );
thisBlockMetrics.maxBlockLeading = std::max( thisBlockMetrics.maxBlockLeading, fm.leading() / scaleFactor );
thisBlockMetrics.maxBlockMaxWidth = std::max( thisBlockMetrics.maxBlockMaxWidth, imageWidth );
@ -465,12 +476,17 @@ void QgsTextDocumentMetrics::processFragment( QgsTextDocumentMetrics &res, const
thisBlockMetrics.blockHeightUsingAscentDescent = std::max( thisBlockMetrics.blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent );
thisBlockMetrics.blockHeightUsingLineSpacing = std::max( thisBlockMetrics.blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, fm.ascent() / scaleFactor );
thisBlockMetrics.maxBlockAscentForTextFragments = std::max( thisBlockMetrics.maxBlockAscentForTextFragments, fm.ascent() / scaleFactor );
const double ascent = fm.ascent() / scaleFactor;
thisBlockMetrics.fragmentAscent << ascent;
thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, ascent );
thisBlockMetrics.maxBlockAscentForTextFragments = std::max( thisBlockMetrics.maxBlockAscentForTextFragments, ascent );
thisBlockMetrics.maxBlockCapHeight = std::max( thisBlockMetrics.maxBlockCapHeight, fm.capHeight() / scaleFactor );
thisBlockMetrics.maxBlockDescent = std::max( thisBlockMetrics.maxBlockDescent, fm.descent() / scaleFactor );
const double descent = fm.descent() / scaleFactor;
thisBlockMetrics.fragmentDescent << descent;
thisBlockMetrics.maxBlockDescent = std::max( thisBlockMetrics.maxBlockDescent, descent );
thisBlockMetrics.maxBlockMaxWidth = std::max( thisBlockMetrics.maxBlockMaxWidth, fm.maxWidth() / scaleFactor );
if ( ( fm.lineSpacing() / scaleFactor ) > thisBlockMetrics.maxLineSpacing )
@ -842,6 +858,16 @@ double QgsTextDocumentMetrics::fragmentFixedHeight( int blockIndex, int fragment
return mFragmentFixedHeights.value( blockIndex ).value( fragmentIndex );
}
double QgsTextDocumentMetrics::fragmentAscent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
{
return mFragmentAscent.value( blockIndex ).value( fragmentIndex );
}
double QgsTextDocumentMetrics::fragmentDescent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
{
return mFragmentDescent.value( blockIndex ).value( fragmentIndex );
}
double QgsTextDocumentMetrics::verticalOrientationXOffset( int blockIndex ) const
{
return mVerticalOrientationXOffsets.value( blockIndex );
@ -857,6 +883,11 @@ double QgsTextDocumentMetrics::blockMaximumDescent( int blockIndex ) const
return mBlockMaxDescent.value( blockIndex );
}
double QgsTextDocumentMetrics::blockMaximumAscent( int blockIndex ) const
{
return mBlockMaxAscent.value( blockIndex );
}
QFont QgsTextDocumentMetrics::fragmentFont( int blockIndex, int fragmentIndex ) const
{
return mFragmentFonts.value( blockIndex ).value( fragmentIndex );

View File

@ -189,6 +189,24 @@ class CORE_EXPORT QgsTextDocumentMetrics
*/
double fragmentFixedHeight( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
/**
* Returns the ascent of the fragment at the specified block and fragment index.
*
* \see fragmentDescent()
*
* \since QGIS 3.42
*/
double fragmentAscent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
/**
* Returns the descent of the fragment at the specified block and fragment index.
*
* \see fragmentAscent()
*
* \since QGIS 3.42
*/
double fragmentDescent( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
/**
* Returns the vertical orientation x offset for the specified block.
*/
@ -201,9 +219,19 @@ class CORE_EXPORT QgsTextDocumentMetrics
/**
* Returns the maximum descent encountered in the specified block.
*
* \see blockMaximumAscent()
*/
double blockMaximumDescent( int blockIndex ) const;
/**
* Returns the maximum ascent encountered in the specified block.
*
* \see blockMaximumDescent()
* \since QGIS 3.42
*/
double blockMaximumAscent( int blockIndex ) const;
/**
* Returns the calculated font for the fragment at the specified block and fragment indices.
*/
@ -277,8 +305,12 @@ class CORE_EXPORT QgsTextDocumentMetrics
QList< QList< double > > mFragmentVerticalOffsetsPointMode;
QList< QList< double > > mFragmentVerticalOffsetsRectMode;
QList< QList< double > > mFragmentAscent;
QList< QList< double > > mFragmentDescent;
QList< double > mVerticalOrientationXOffsets;
QList< double > mBlockMaxDescent;
QList< double > mBlockMaxAscent;
QList< double > mBlockMaxCharacterWidth;
double mFirstLineAscentOffset = 0;
double mLastLineAscentOffset = 0;

View File

@ -1857,12 +1857,124 @@ QVector< QgsTextRenderer::BlockMetrics > QgsTextRenderer::calculateBlockMetrics(
{
thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
}
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
thisBlockMetrics.backgroundWidth = targetWidth;
thisBlockMetrics.backgroundXOffset = 0;
break;
case Qgis::TextLayoutMode::Point:
case Qgis::TextLayoutMode::Labeling:
thisBlockMetrics.backgroundWidth = thisBlockMetrics.width;
thisBlockMetrics.backgroundXOffset = thisBlockMetrics.xOffset;
break;
}
blockMetrics << thisBlockMetrics;
blockIndex++;
}
return blockMetrics;
}
QBrush QgsTextRenderer::createBrushForPath( QgsRenderContext &context, const QString &path )
{
bool fitsInCache = false;
// use original image size
const QSize imageSize = QgsApplication::imageCache()->originalSize( path, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
// TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
const double pixelsPerMm = context.scaleFactor();
const double imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
const double imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
QBrush res;
if ( imageWidth == 0 || imageHeight == 0 )
return res;
const QImage image = QgsApplication::imageCache()->pathAsImage( path,
QSize( static_cast< int >( std::round( imageWidth ) ),
static_cast< int >( std::round( imageHeight ) ) ),
false,
1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
if ( !image.isNull() )
{
res.setTextureImage( image );
}
return res;
}
void QgsTextRenderer::renderDocumentBackgrounds( QgsRenderContext &context, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, const Component &component, const QVector< QgsTextRenderer::BlockMetrics > &blockMetrics, Qgis::TextLayoutMode mode, double verticalAlignOffset, double rotation )
{
int blockIndex = 0;
context.painter()->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
context.painter()->rotate( rotation );
context.painter()->setPen( Qt::NoPen );
context.painter()->setBrush( Qt::NoBrush );
for ( const QgsTextBlock &block : document )
{
const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
const double blockMaximumDescent = metrics.blockMaximumDescent( blockIndex );
const double blockMaximumAscent = metrics.blockMaximumAscent( blockIndex );
if ( block.blockFormat().hasBackground() )
{
QBrush backgroundBrush = block.blockFormat().backgroundBrush();
if ( !block.blockFormat().backgroundImagePath().isEmpty() )
{
const QBrush backgroundImageBrush = createBrushForPath( context, block.blockFormat().backgroundImagePath() );
if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
backgroundBrush = backgroundImageBrush;
}
context.painter()->setBrush( backgroundBrush );
context.painter()->drawRect( QRectF( blockMetrics[ blockIndex ].backgroundXOffset, baseLineOffset - blockMaximumAscent, blockMetrics[ blockIndex ].backgroundWidth, blockMaximumDescent + blockMaximumAscent ) );
}
double xOffset = 0;
int fragmentIndex = 0;
for ( const QgsTextFragment &fragment : block )
{
const double horizontalAdvance = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
const double ascent = metrics.fragmentAscent( blockIndex, fragmentIndex, mode );
const double descent = metrics.fragmentDescent( blockIndex, fragmentIndex, mode );
if ( fragment.characterFormat().hasBackground() )
{
const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
QBrush backgroundBrush = fragment.characterFormat().backgroundBrush();
if ( !fragment.characterFormat().backgroundImagePath().isEmpty() )
{
const QBrush backgroundImageBrush = createBrushForPath( context, fragment.characterFormat().backgroundImagePath() );
if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
backgroundBrush = backgroundImageBrush;
}
context.painter()->setBrush( backgroundBrush );
context.painter()->drawRect( QRectF( blockMetrics[ blockIndex ].xOffset + xOffset,
baseLineOffset + verticalAlignOffset + yOffset - ascent, horizontalAdvance, ascent + descent ) );
}
xOffset += horizontalAdvance;
fragmentIndex ++;
}
blockIndex++;
}
context.painter()->setBrush( Qt::NoBrush );
if ( !qgsDoubleNear( rotation, 0.0 ) )
context.painter()->rotate( -rotation );
context.painter()->translate( -component.origin );
}
void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, Qgis::TextLayoutMode mode, const Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, const Qgis::TextHorizontalAlignment hAlignment,
Qgis::TextVerticalAlignment vAlignment, double rotation )
{
@ -1933,6 +2045,12 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
|| ( components & Qgis::TextComponent::Shadow ) )
{
const QVector< BlockMetrics > blockMetrics = calculateBlockMetrics( document, metrics, mode, targetWidth, hAlignment );
if ( document.hasBackgrounds() )
{
renderDocumentBackgrounds( context, document, metrics, component, blockMetrics, mode, verticalAlignOffset, rotation );
}
int blockIndex = 0;
for ( const QgsTextBlock &block : document )
{

View File

@ -519,7 +519,9 @@ class CORE_EXPORT QgsTextRenderer
struct BlockMetrics
{
double xOffset = 0;
double backgroundXOffset = 0;
double width = 0;
double backgroundWidth = 0;
double extraWordSpace = 0;
double extraLetterSpace = 0;
};
@ -533,6 +535,8 @@ class CORE_EXPORT QgsTextRenderer
QVector< DeferredRenderFragment > fragments;
};
static QBrush createBrushForPath( QgsRenderContext &context, const QString &path );
static void renderBlockHorizontal( const QgsTextBlock &block, int blockIndex,
const QgsTextDocumentMetrics &metrics, QgsRenderContext &context,
const QgsTextFormat &format,
@ -540,6 +544,7 @@ class CORE_EXPORT QgsTextRenderer
double fontScale, double extraWordSpace, double extraLetterSpace,
Qgis::TextLayoutMode mode,
DeferredRenderBlock *deferredRenderBlock );
static void renderDocumentBackgrounds( QgsRenderContext &context, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, const Component &component, const QVector< QgsTextRenderer::BlockMetrics > &blockMetrics, Qgis::TextLayoutMode mode, double verticalAlignOffset, double rotation );
static void renderDeferredBlocks( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, const std::vector<DeferredRenderBlock> &deferredBlocks, bool usePathsForText, double fontScale, const Component &component, double rotation );
static void renderDeferredBuffer( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, const std::vector<DeferredRenderBlock> &deferredBlocks, double fontScale, const Component &component, double rotation );
static void renderDeferredShadowForText( QgsRenderContext &context, const QgsTextFormat &format, const std::vector<DeferredRenderBlock> &deferredBlocks, double fontScale, const Component &component, double rotation );

View File

@ -61,6 +61,11 @@ class TestQgsTextBlockFormat(QgisTestCase):
self.assertTrue(format.hasBackground())
self.assertEqual(format.backgroundBrush().color().name(), '#ffff00')
format = QgsTextBlockFormat()
format.setBackgroundImagePath('test')
self.assertTrue(format.hasBackground())
self.assertEqual(format.backgroundImagePath(), 'test')
def testUpdateFont(self):
context = QgsRenderContext()
font = QgsFontUtils.getStandardTestFont()

View File

@ -80,6 +80,11 @@ class TestQgsTextCharacterFormat(QgisTestCase):
self.assertTrue(format.hasBackground())
self.assertEqual(format.backgroundBrush().color().name(), '#ffff00')
format = QgsTextCharacterFormat()
format.setBackgroundImagePath('test')
self.assertTrue(format.hasBackground())
self.assertEqual(format.backgroundImagePath(), 'test')
def testUpdateFont(self):
context = QgsRenderContext()
font = QgsFontUtils.getStandardTestFont()

View File

@ -356,6 +356,23 @@ class TestQgsTextDocument(QgisTestCase):
self.assertFalse(doc.hasBackgrounds())
self.assertFalse(doc[0].hasBackgrounds())
# background paths
doc = QgsTextDocument.fromHtml(['<div style="background-image: url(something)"><span style="background-color: red;">red</span><span style="background-image: url(something_else);">yellow</span>outside span</div>'])
self.assertTrue(doc.hasBackgrounds())
self.assertEqual(len(doc), 1)
# there's a bug in Qt's css parsing here -- the background-image incorrectly gets attached to the spans, not the div!
# self.assertTrue(doc[0].blockFormat().hasBackground())
self.assertTrue(doc[0].hasBackgrounds())
self.assertTrue(doc[0][0].characterFormat().hasBackground())
self.assertEqual(doc[0][0].characterFormat().backgroundBrush().color().name(), '#ff0000')
self.assertFalse(doc[0][0].characterFormat().backgroundImagePath())
self.assertTrue(doc[0][1].characterFormat().hasBackground())
self.assertEqual(doc[0][1].characterFormat().backgroundImagePath(), 'something_else')
self.assertTrue(doc[0][2].characterFormat().hasBackground())
self.assertEqual(doc[0][2].characterFormat().backgroundImagePath(),
'something')
def testFromTextAndFormat(self):
format = QgsTextFormat()
format.setAllowHtmlFormatting(False)

View File

@ -20,7 +20,7 @@ from qgis.PyQt.QtCore import (
QRectF,
QSize,
QSizeF,
Qt,
Qt
)
from qgis.PyQt.QtGui import (
QBrush,
@ -307,6 +307,48 @@ class PyQgsTextRenderer(QgisTestCase):
self.assertTrue(self.image_check('draw_document_rect', 'draw_document_rect', image, 'draw_document_rect'))
def testDrawRectHtmlBackground(self):
"""
Test drawing html with backgrounds in rect mode
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setAllowHtmlFormatting(True)
format.setSize(16)
self.assertTrue(self.checkRender(format, 'html_background_rect', rect=QRectF(10, 100, 300, 100), text=['<div style="background-color: blue"><span style="background-color: rgba(255,0,0,0.5);">red</span> <span style="font-size: 10pt; background-color: yellow;">yellow</span> outside span</div><div style="background-color: pink; text-align: right;">no span <span style="background-color: yellow;">yel</span> no bg</div>']))
def testDrawPointHtmlBackground(self):
"""
Test drawing html with backgrounds in point mode
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setAllowHtmlFormatting(True)
format.setSize(16)
self.assertTrue(self.checkRenderPoint(format, 'html_background_point', point=QPointF(10, 100), text=['<div style="background-color: blue"><span style="background-color: rgba(255,0,0,0.5);">red</span> <span style="font-size: 10pt; background-color: yellow;">yellow</span> outside span</div><div style="background-color: pink; ext-align: right;">no span <span style="background-color: yellow;">yel</span> no bg</div>']))
def testDrawRectHtmlBackgroundImage(self):
"""
Test drawing html with background image in rect mode
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setAllowHtmlFormatting(True)
format.setSize(36)
image_url = unitTestDataPath() + "/raster_brush.png"
self.assertTrue(self.checkRender(format, 'html_background_image_rect', rect=QRectF(10, 100, 300, 100), text=[f'<div style="background-image: url({image_url})">test text</div>']))
def testDrawPointHtmlBackgroundImage(self):
"""
Test drawing html with backgrounds in point mode
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setAllowHtmlFormatting(True)
format.setSize(36)
image_url = unitTestDataPath() + "/raster_brush.png"
self.assertTrue(self.checkRenderPoint(format, 'html_background_image_point', point=QPointF(10, 100), text=[f'<div style="background-image: url({image_url})">test text</div>']))
def testDrawRectCapHeightMode(self):
"""
Test drawing text in rect mode with cap height based line heights

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB