Skip to content
Permalink
Browse files

[FEATURE] Allow use of color related HTML font tags in label text

When enabled, this option will treat label contents as HTML, and
any FOREGROUND COLOR RELATED html formatting options will be
respected in the rendered label.

I repeat: ONLY HTML COLOR TAGS ARE RESPECTED. This is NOT a bug,
rather this feature has been designed as a "test of the waters"
with HTML formatting in labels, and accordingly only formatting
options which do not alter the font shape were considered.

On the plus side, it works correctly with ALL other label settings,
including shadows, buffers, curved labels, etc!

Sponsored by geoProRegio AG (www.geoproregio.ch)
  • Loading branch information
nyalldawson committed May 8, 2020
1 parent f5d68ff commit d4fbb6c3d632445097edf98bf2987ca20f64dd79
@@ -2019,6 +2019,7 @@ Draws a single component of rendered text using the specified settings.
as of QGIS 3.4.3 and the text format should be set using QgsRenderContext.setTextRenderFormat() instead.
%End


static void drawPart( QPointF origin, double rotation, HAlignment alignment, const QStringList &textLines,
QgsRenderContext &context, const QgsTextFormat &format,
TextPart part, bool drawAsOutlines = true );
@@ -2040,6 +2041,7 @@ Draws a single component of rendered text using the specified settings.
as of QGIS 3.4.3 and the text format should be set using QgsRenderContext.setTextRenderFormat() instead.
%End


static QFontMetricsF fontMetrics( QgsRenderContext &context, const QgsTextFormat &format );
%Docstring
Returns the font metrics for the given text ``format``, when rendered
@@ -2470,7 +2470,7 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
// TODO: only for placement which needs character info
// account for any data defined font metrics adjustments
lf->calculateInfo( placement == QgsPalLayerSettings::Curved || placement == QgsPalLayerSettings::PerimeterCurved,
labelFontMetrics.get(), xform, maxcharanglein, maxcharangleout );
labelFontMetrics.get(), xform, maxcharanglein, maxcharangleout, format().allowHtmlFormatting() );
// for labelFeature the LabelInfo is passed to feat when it is registered

// TODO: allow layer-wide feature dist in PAL...?
@@ -19,7 +19,7 @@
#include "qgspallabeling.h"
#include "qgsmaptopixel.h"
#include "pal/feature.h"

#include <QTextDocument>

QgsTextLabelFeature::QgsTextLabelFeature( QgsFeatureId id, geos::unique_ptr geometry, QSizeF size )
: QgsLabelFeature( id, std::move( geometry ), size )
@@ -42,8 +42,17 @@ QString QgsTextLabelFeature::text( int partId ) const
return mClusters.at( partId );
}

QTextCharFormat QgsTextLabelFeature::characterFormat( int partId ) const
{
return mCharacterFormats.value( partId );
}

void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm, const QgsMapToPixel *xform, double maxinangle, double maxoutangle )
bool QgsTextLabelFeature::hasCharacterFormat( int partId ) const
{
return partId < mCharacterFormats.size();
}

void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm, const QgsMapToPixel *xform, double maxinangle, double maxoutangle, bool allowHtmlTags )
{
if ( mInfo )
return;
@@ -72,8 +81,41 @@ void QgsTextLabelFeature::calculateInfo( bool curvedLabeling, QFontMetricsF *fm,
qreal charWidth;
qreal wordSpaceFix;

//split string by valid grapheme boundaries - required for certain scripts (see #6883)
mClusters = QgsPalLabeling::splitToGraphemes( mLabelText );

if ( allowHtmlTags && curvedLabeling )
{
QTextDocument doc;
doc.setHtml( mLabelText );

QTextBlock block = doc.firstBlock();
while ( true )
{
auto it = block.begin();
QString blockText;
while ( !it.atEnd() )
{
const QTextFragment fragment = it.fragment();
if ( fragment.isValid() )
{
const QStringList graphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
for ( const QString &grapheme : graphemes )
{
mClusters.append( grapheme );
mCharacterFormats.append( fragment.charFormat() );
}
}
it++;
}
block = block.next();
if ( !block.isValid() )
break;
}
}
else
{
//split string by valid grapheme boundaries - required for certain scripts (see #6883)
mClusters = QgsPalLabeling::splitToGraphemes( mLabelText );
}

mInfo = new pal::LabelInfo( mClusters.count(), labelHeight, maxinangle, maxoutangle );
for ( int i = 0; i < mClusters.count(); i++ )
@@ -42,8 +42,25 @@ class QgsTextLabelFeature : public QgsLabelFeature
*/
QString text( int partId ) const;

/**
* Returns the character format corresponding to the specified label part
* \param partId Set to the required part index for labels which are broken into parts (curved labels)
*
* This only returns valid formats for curved label placements.
*
* \since QGIS 3.14
*/
QTextCharFormat characterFormat( int partId ) const;

/**
* Returns TRUE if the feature contains specific character formatting for the part with matching ID.
*
* \since QGIS 3.14
*/
bool hasCharacterFormat( int partId ) const;

//! calculate data for info(). setDefinedFont() must have been called already.
void calculateInfo( bool curvedLabeling, QFontMetricsF *fm, const QgsMapToPixel *xform, double maxinangle, double maxoutangle );
void calculateInfo( bool curvedLabeling, QFontMetricsF *fm, const QgsMapToPixel *xform, double maxinangle, double maxoutangle, bool allowHtmlTags );

//! Gets data-defined values
const QMap< QgsPalLayerSettings::Property, QVariant > &dataDefinedValues() const { return mDataDefinedValues; }
@@ -61,6 +78,9 @@ class QgsTextLabelFeature : public QgsLabelFeature
protected:
//! List of graphemes (used for curved labels)
QStringList mClusters;

QList< QTextCharFormat > mCharacterFormats;

//! Font for rendering
QFont mDefinedFont;
//! Metrics of the font for rendering
@@ -36,6 +36,8 @@
#include "pal/layer.h"

#include <QPicture>
#include <QTextDocument>
#include <QTextFragment>

using namespace pal;

@@ -610,8 +612,6 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
}
}

//QgsDebugMsgLevel( "drawLabel " + txt, 4 );
QStringList multiLineList = QgsPalLabeling::splitToLines( txt, tmpLyr.wrapChar, tmpLyr.autoWrapLength, tmpLyr.useMaxLineLengthForAutoWrap, tmpLyr.format().allowHtmlFormatting() );

QgsTextRenderer::HAlignment hAlign = QgsTextRenderer::AlignLeft;
if ( tmpLyr.multilineAlign == QgsPalLayerSettings::MultiCenter )
@@ -623,7 +623,68 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
component.origin = outPt;
component.rotation = label->getAlpha();

QgsTextRenderer::drawTextInternal( drawType, context, tmpLyr.format(), component, multiLineList, labelfm,
QList< QgsTextRenderer::TextBlock > textBlocks;
if ( !tmpLyr.format().allowHtmlFormatting() || tmpLyr.placement == QgsPalLayerSettings::Curved )
{
QTextCharFormat c;
if ( lf->hasCharacterFormat( label->getPartId() ) )
{
c = lf->characterFormat( label->getPartId() );
}
else
{
QBrush b;
c = QTextCharFormat();
b.setColor( tmpLyr.format().color() );
c.setForeground( b );
}
const QStringList multiLineList = QgsPalLabeling::splitToLines( txt, tmpLyr.wrapChar, tmpLyr.autoWrapLength, tmpLyr.useMaxLineLengthForAutoWrap );
for ( const QString line : multiLineList )
textBlocks << ( QgsTextRenderer::TextBlock() << QgsTextRenderer::TextFragment( line, c ) );
}
else
{
QTextDocument doc;
doc.setHtml( QStringLiteral( "<span style=\"color: %1\">%2</span>" ).arg( tmpLyr.format().color().name(), txt ) );
QTextBlock block = doc.firstBlock();
while ( true )
{
QgsTextRenderer::TextBlock blockFragments;
auto it = block.begin();
while ( !it.atEnd() )
{
const QTextFragment fragment = it.fragment();
if ( fragment.isValid() )
{
const QStringList multiLineList = QgsPalLabeling::splitToLines( fragment.text(), tmpLyr.wrapChar, tmpLyr.autoWrapLength, tmpLyr.useMaxLineLengthForAutoWrap );
if ( multiLineList.size() > 1 )
{
// split this fragment over multiple blocks
blockFragments << QgsTextRenderer::TextFragment( multiLineList.at( 0 ), fragment.charFormat() );
for ( int lineIndex = 1; lineIndex < multiLineList.size(); ++ lineIndex )
{
textBlocks << blockFragments;
blockFragments.clear();
blockFragments << QgsTextRenderer::TextFragment( multiLineList.at( lineIndex ), fragment.charFormat() );
}
}
else
{
blockFragments << QgsTextRenderer::TextFragment( fragment.text(), fragment.charFormat() );
}
}
it++;
}

textBlocks << blockFragments;

block = block.next();
if ( !block.isValid() )
break;
}
}

QgsTextRenderer::drawTextInternal( drawType, context, tmpLyr.format(), component, textBlocks, labelfm,
hAlign, QgsTextRenderer::Label );

}
@@ -2705,6 +2705,14 @@ QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format

void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, HAlignment alignment,
const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, QgsTextRenderer::TextPart part, bool )
{
QList<TextBlock> blocks;
for ( const QString &line : textLines )
blocks << ( TextBlock() << TextFragment( line ) );
drawPart( rect, rotation, alignment, blocks, context, format, part );
}

void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, QgsTextRenderer::HAlignment alignment, const QList<TextBlock> &textLines, QgsRenderContext &context, const QgsTextFormat &format, QgsTextRenderer::TextPart part )
{
if ( !context.painter() )
{
@@ -2743,7 +2751,18 @@ void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, HAlignment
component.center = rect.center();
}

QgsTextRenderer::drawBackground( context, component, format, textLines, Rect );
QStringList lines;
for ( const TextBlock &block : textLines )
{
QString line;
for ( const TextFragment &fragment : block )
{
line.append( fragment.text );
}
lines << line;
}

QgsTextRenderer::drawBackground( context, component, format, lines, Rect );

break;
}
@@ -2768,6 +2787,14 @@ void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, HAlignment
}

void QgsTextRenderer::drawPart( QPointF origin, double rotation, QgsTextRenderer::HAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, QgsTextRenderer::TextPart part, bool )
{
QList<TextBlock> blocks;
for ( const QString &line : textLines )
blocks << ( TextBlock() << TextFragment( line ) );
drawPart( origin, rotation, alignment, blocks, context, format, part );
}

void QgsTextRenderer::drawPart( QPointF origin, double rotation, QgsTextRenderer::HAlignment alignment, const QList<QgsTextRenderer::TextBlock> &textLines, QgsRenderContext &context, const QgsTextFormat &format, QgsTextRenderer::TextPart part )
{
if ( !context.painter() )
{
@@ -2787,7 +2814,17 @@ void QgsTextRenderer::drawPart( QPointF origin, double rotation, QgsTextRenderer
if ( !format.background().enabled() )
return;

QgsTextRenderer::drawBackground( context, component, format, textLines, Point );
QStringList lines;
for ( const TextBlock &block : textLines )
{
QString line;
for ( const TextFragment &fragment : block )
{
line.append( fragment.text );
}
lines << line;
}
QgsTextRenderer::drawBackground( context, component, format, lines, Point );
break;
}

@@ -3614,7 +3651,7 @@ void QgsTextRenderer::drawTextInternal( TextPart drawType,
QgsRenderContext &context,
const QgsTextFormat &format,
const Component &component,
const QStringList &textLines,
const QList< TextBlock > &blocks,
const QFontMetricsF *fontMetrics,
HAlignment alignment, DrawMode mode )
{
@@ -3646,6 +3683,17 @@ void QgsTextRenderer::drawTextInternal( TextPart drawType,
}
}

QStringList textLines;
for ( const TextBlock &block : blocks )
{
QString line;
for ( const TextFragment &fragment : block )
{
line.append( fragment.text );
}
textLines << line;
}

switch ( orientation )
{
case QgsTextFormat::HorizontalOrientation:
@@ -3683,6 +3731,8 @@ void QgsTextRenderer::drawTextInternal( TextPart drawType,
const auto constTextLines = textLines;
for ( const QString &line : constTextLines )
{
const TextBlock block = blocks.at( i );

context.painter()->save();
if ( context.flags() & QgsRenderContext::Antialiasing )
{
@@ -3774,25 +3824,37 @@ void QgsTextRenderer::drawTextInternal( TextPart drawType,
}
else
{
// draw text, QPainterPath method
QPainterPath path;
path.setFillRule( Qt::WindingFill );
path.addText( 0, 0, format.scaledFont( context ), subComponent.text );

// store text's drawing in QPicture for drop shadow call
QPicture textPict;
QPainter textp;
textp.begin( &textPict );
textp.setPen( Qt::NoPen );
QColor textColor = format.color();
textColor.setAlphaF( format.opacity() );
textp.setBrush( textColor );
textp.drawPath( path );
// TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
// e.g. some capitalization options, but not others
//textp.setFont( tmpLyr.textFont );
//textp.setPen( tmpLyr.textColor );
//textp.drawText( 0, 0, component.text() );
QFont font = format.scaledFont( context );

double xOffset = 0;
for ( const TextFragment &fragment : block )
{
// draw text, QPainterPath method
QPainterPath path;
path.setFillRule( Qt::WindingFill );
path.addText( xOffset, 0, font, fragment.text );

QColor textColor = fragment.charFormat.foreground().color();
textColor.setAlphaF( format.opacity() );
textp.setBrush( textColor );
textp.drawPath( path );

#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
xOffset += fontMetrics->width( fragment.text );
#else
xOffset += fontMetrics->horizontalAdvance( fragment.text );
#endif
// TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
// e.g. some capitalization options, but not others
//textp.setFont( tmpLyr.textFont );
//textp.setPen( tmpLyr.textColor );
//textp.drawText( 0, 0, component.text() );
}
textp.end();

if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )

0 comments on commit d4fbb6c

Please sign in to comment.
You can’t perform that action at this time.