QGIS/src/core/textrenderer/qgstextrenderer.cpp
2025-07-12 20:21:10 +02:00

2660 lines
107 KiB
C++

/***************************************************************************
qgstextrenderer.cpp
-------------------
begin : September 2015
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 "qgstextrenderer.h"
#include "qgstextformat.h"
#include "qgstextdocument.h"
#include "qgstextdocumentmetrics.h"
#include "qgstextfragment.h"
#include "qgspallabeling.h"
#include "qgspainteffect.h"
#include "qgspainterswapper.h"
#include "qgsmarkersymbollayer.h"
#include "qgssymbollayerutils.h"
#include "qgsmarkersymbol.h"
#include "qgsfillsymbol.h"
#include "qgsunittypes.h"
#include "qgstextmetrics.h"
#include "qgstextrendererutils.h"
#include "qgsgeos.h"
#include "qgspainting.h"
#include "qgsapplication.h"
#include "qgsimagecache.h"
#include <optional>
#include <QTextBoundaryFinder>
Qgis::TextHorizontalAlignment QgsTextRenderer::convertQtHAlignment( Qt::Alignment alignment )
{
if ( alignment & Qt::AlignLeft )
return Qgis::TextHorizontalAlignment::Left;
else if ( alignment & Qt::AlignRight )
return Qgis::TextHorizontalAlignment::Right;
else if ( alignment & Qt::AlignHCenter )
return Qgis::TextHorizontalAlignment::Center;
else if ( alignment & Qt::AlignJustify )
return Qgis::TextHorizontalAlignment::Justify;
// not supported?
return Qgis::TextHorizontalAlignment::Left;
}
Qgis::TextVerticalAlignment QgsTextRenderer::convertQtVAlignment( Qt::Alignment alignment )
{
if ( alignment & Qt::AlignTop )
return Qgis::TextVerticalAlignment::Top;
else if ( alignment & Qt::AlignBottom )
return Qgis::TextVerticalAlignment::Bottom;
else if ( alignment & Qt::AlignVCenter )
return Qgis::TextVerticalAlignment::VerticalCenter;
//not supported
else if ( alignment & Qt::AlignBaseline )
return Qgis::TextVerticalAlignment::Bottom;
return Qgis::TextVerticalAlignment::Top;
}
int QgsTextRenderer::sizeToPixel( double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale )
{
return static_cast< int >( c.convertToPainterUnits( size, unit, mapUnitScale ) + 0.5 ); //NOLINT
}
void QgsTextRenderer::drawText( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &text, QgsRenderContext &context, const QgsTextFormat &_format, bool, Qgis::TextVerticalAlignment vAlignment, Qgis::TextRendererFlags flags,
Qgis::TextLayoutMode mode )
{
QgsTextFormat lFormat = _format;
if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use format instead of tmpFormat here, it's const and potentially avoids a detach
lFormat.updateDataDefinedProperties( context );
// DO NOT USE _format in the following code, always use lFormat!!
QgsTextDocumentRenderContext documentContext;
documentContext.setFlags( flags );
documentContext.setMaximumWidth( rect.width() );
const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( text, lFormat );
const double fontScale = calculateScaleFactorForFormat( context, lFormat );
const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale, documentContext );
drawDocument( rect, lFormat, metrics.document(), metrics, context, alignment, vAlignment, rotation, mode, flags );
}
void QgsTextRenderer::drawDocument( const QRectF &rect, const QgsTextFormat &format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment horizontalAlignment, Qgis::TextVerticalAlignment verticalAlignment, double rotation, Qgis::TextLayoutMode mode, Qgis::TextRendererFlags )
{
const QgsTextFormat tmpFormat = updateShadowPosition( format );
Qgis::TextComponents components = Qgis::TextComponent::Text;
if ( tmpFormat.background().enabled() )
{
components |= Qgis::TextComponent::Background;
}
if ( tmpFormat.shadow().enabled() )
{
components |= Qgis::TextComponent::Shadow;
}
if ( tmpFormat.buffer().enabled() )
{
components |= Qgis::TextComponent::Buffer;
}
drawParts( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, components, mode );
}
void QgsTextRenderer::drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &_format, bool )
{
QgsTextFormat lFormat = _format;
if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
lFormat.updateDataDefinedProperties( context );
lFormat = updateShadowPosition( lFormat );
// DO NOT USE _format in the following code, always use lFormat!!
const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, lFormat );
const double fontScale = calculateScaleFactorForFormat( context, lFormat );
const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
drawDocument( point, lFormat, metrics.document(), metrics, context, alignment, rotation );
}
void QgsTextRenderer::drawDocument( QPointF point, const QgsTextFormat &_format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment alignment, double rotation, Qgis::TextLayoutMode mode )
{
const QgsTextFormat lFormat = updateShadowPosition( _format );
// DO NOT USE _format in the following code, always use lFormat!!
Qgis::TextComponents components = Qgis::TextComponent::Text;
if ( lFormat.background().enabled() )
{
components |= Qgis::TextComponent::Background;
}
if ( lFormat.shadow().enabled() )
{
components |= Qgis::TextComponent::Shadow;
}
if ( lFormat.buffer().enabled() )
{
components |= Qgis::TextComponent::Buffer;
}
drawParts( point, rotation, alignment, document, metrics, context, lFormat, components, mode );
}
void QgsTextRenderer::drawTextOnLine( const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &_format, double offsetAlongLine, double offsetFromLine )
{
QgsTextFormat lFormat = _format;
if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
lFormat.updateDataDefinedProperties( context );
lFormat = updateShadowPosition( lFormat );
// DO NOT USE _format in the following code, always use lFormat!!
// todo handle newlines??
const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( {text}, lFormat );
drawDocumentOnLine( line, lFormat, document, context, offsetAlongLine, offsetFromLine );
}
void QgsTextRenderer::drawDocumentOnLine( const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine, double offsetFromLine )
{
QPolygonF labelBaselineCurve = line;
if ( !qgsDoubleNear( offsetFromLine, 0 ) )
{
std::unique_ptr < QgsLineString > ring( QgsLineString::fromQPolygonF( line ) );
QgsGeos geos( ring.get() );
std::unique_ptr < QgsLineString > offsetCurve( dynamic_cast< QgsLineString * >( geos.offsetCurve( offsetFromLine, 4, Qgis::JoinStyle::Round, 2 ) ) );
if ( !offsetCurve )
return;
#if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<11
if ( offsetFromLine < 0 )
{
// geos < 3.11 reverses the direction of offset curves with negative distances -- we don't want that!
std::unique_ptr < QgsLineString > reversed( offsetCurve->reversed() );
if ( !reversed )
return;
offsetCurve = std::move( reversed );
}
#endif
labelBaselineCurve = offsetCurve->asQPolygonF();
}
const double fontScale = calculateScaleFactorForFormat( context, format );
const QFont baseFont = format.scaledFont( context, fontScale );
const double letterSpacing = baseFont.letterSpacing() / fontScale;
const double wordSpacing = baseFont.wordSpacing() / fontScale;
QStringList graphemes;
QVector< QgsTextCharacterFormat > graphemeFormats;
QVector< QgsTextDocumentMetrics > graphemeMetrics;
for ( const QgsTextBlock &block : std::as_const( document ) )
{
for ( const QgsTextFragment &fragment : block )
{
const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
for ( const QString &grapheme : fragmentGraphemes )
{
graphemes.append( grapheme );
graphemeFormats.append( fragment.characterFormat() );
QgsTextDocument document;
document.append( QgsTextBlock( QgsTextFragment( grapheme, fragment.characterFormat() ) ) );
graphemeMetrics.append( QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale ) );
}
}
}
QVector< double > characterWidths( graphemes.count() );
QVector< double > characterHeights( graphemes.count() );
QVector< double > characterDescents( graphemes.count() );
QFont previousNonSuperSubScriptFont;
for ( int i = 0; i < graphemes.count(); i++ )
{
// reconstruct how Qt creates word spacing, then adjust per individual stored character
// this will allow the text renderer to create each candidate width = character width + correct spacing
double graphemeFirstCharHorizontalAdvanceWithLetterSpacing = 0;
double graphemeFirstCharHorizontalAdvance = 0;
double graphemeHorizontalAdvance = 0;
double characterDescent = 0;
double characterHeight = 0;
const QgsTextCharacterFormat *graphemeFormat = &graphemeFormats[i];
QFont graphemeFont = baseFont;
graphemeFormat->updateFontForFormat( graphemeFont, context, fontScale );
if ( i == 0 )
previousNonSuperSubScriptFont = graphemeFont;
if ( graphemeFormat->hasVerticalAlignmentSet() )
{
switch ( graphemeFormat->verticalAlignment() )
{
case Qgis::TextCharacterVerticalAlignment::Normal:
previousNonSuperSubScriptFont = graphemeFont;
break;
case Qgis::TextCharacterVerticalAlignment::SuperScript:
case Qgis::TextCharacterVerticalAlignment::SubScript:
{
if ( graphemeFormat->fontPointSize() < 0 )
{
// if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
// this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
// the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
// that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
// which we should respect
graphemeFont.setPixelSize( static_cast< int >( std::round( graphemeFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
}
break;
}
}
}
else
{
previousNonSuperSubScriptFont = graphemeFont;
}
const QFontMetricsF graphemeFontMetrics( graphemeFont );
graphemeFirstCharHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ) / fontScale;
graphemeFirstCharHorizontalAdvanceWithLetterSpacing = graphemeFontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) / fontScale + letterSpacing;
graphemeHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i] ) ) / fontScale;
characterDescent = graphemeFontMetrics.descent() / fontScale;
characterHeight = graphemeFontMetrics.height() / fontScale;
qreal wordSpaceFix = qreal( 0.0 );
if ( graphemes[i] == QLatin1String( " " ) )
{
// word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
int nxt = i + 1;
wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != QLatin1String( " " ) ) ? wordSpacing : qreal( 0.0 );
}
// this workaround only works for clusters with a single character. Not sure how it should be handled
// with multi-character clusters.
if ( graphemes[i].length() == 1 &&
!qgsDoubleNear( graphemeFirstCharHorizontalAdvance, graphemeFirstCharHorizontalAdvanceWithLetterSpacing ) )
{
// word spacing applied when it shouldn't be
wordSpaceFix -= wordSpacing;
}
const double charWidth = graphemeHorizontalAdvance + wordSpaceFix;
characterWidths[i] = charWidth;
characterHeights[i] = characterHeight;
characterDescents[i] = characterDescent;
}
QgsPrecalculatedTextMetrics metrics( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) );
metrics.setGraphemeFormats( graphemeFormats );
std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement = QgsTextRendererUtils::generateCurvedTextPlacement(
metrics, labelBaselineCurve, offsetAlongLine,
QgsTextRendererUtils::RespectPainterOrientation,
-1, -1,
QgsTextRendererUtils::CurvedTextFlag::UseBaselinePlacement
| QgsTextRendererUtils::CurvedTextFlag::TruncateStringWhenLineIsTooShort
);
if ( placement->graphemePlacement.empty() )
return;
// We may have deliberately skipped over some graphemes during curved text placement (such as zero-width graphemes).
// So we need to use a hash of the original grapheme index to place generated components in, as there may accordingly
// be graphemes which don't result in components, and we can't just blindly assume the component array position
// will match the original grapheme index
QHash< int, QgsTextRenderer::Component > components;
components.reserve( placement->graphemePlacement.size() );
for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
{
QgsTextRenderer::Component component;
component.origin = QPointF( grapheme.x, grapheme.y );
component.rotation = -grapheme.angle;
QgsTextDocumentMetrics &metrics = graphemeMetrics[ grapheme.graphemeIndex ];
const double verticalOffset = metrics.fragmentVerticalOffset( 0, 0, Qgis::TextLayoutMode::Point );
if ( !qgsDoubleNear( verticalOffset, 0 ) )
{
component.origin.rx() += verticalOffset * std::cos( grapheme.angle + M_PI_2 );
component.origin.ry() += verticalOffset * std::sin( grapheme.angle + M_PI_2 );
}
components.insert( grapheme.graphemeIndex, component );
}
if ( format.background().enabled() )
{
for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
{
const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Point );
}
}
if ( format.buffer().enabled() )
{
for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
{
const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
drawTextInternal( Qgis::TextComponent::Buffer,
context,
format,
component,
metrics.document(),
metrics,
Qgis::TextHorizontalAlignment::Left,
Qgis::TextVerticalAlignment::Top,
Qgis::TextLayoutMode::Point );
}
}
for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
{
const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
drawTextInternal( Qgis::TextComponent::Text,
context,
format,
component,
metrics.document(),
metrics,
Qgis::TextHorizontalAlignment::Left,
Qgis::TextVerticalAlignment::Top,
Qgis::TextLayoutMode::Point );
}
}
QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format )
{
if ( !format.shadow().enabled() || format.shadow().shadowPlacement() != QgsTextShadowSettings::ShadowLowest )
return format;
QgsTextFormat tmpFormat = format;
if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds
{
tmpFormat.shadow().setShadowPlacement( QgsTextShadowSettings::ShadowShape );
}
else if ( tmpFormat.buffer().enabled() )
{
tmpFormat.shadow().setShadowPlacement( QgsTextShadowSettings::ShadowBuffer );
}
else
{
tmpFormat.shadow().setShadowPlacement( QgsTextShadowSettings::ShadowText );
}
return tmpFormat;
}
void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment,
const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
{
const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
const double fontScale = calculateScaleFactorForFormat( context, format );
const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
drawParts( rect, rotation, alignment, Qgis::TextVerticalAlignment::Top, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Rectangle );
}
void QgsTextRenderer::drawParts( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, Qgis::TextVerticalAlignment vAlignment, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents parts, Qgis::TextLayoutMode mode )
{
if ( !context.painter() )
{
return;
}
Component component;
component.dpiRatio = 1.0;
component.origin = rect.topLeft();
component.rotation = rotation;
component.size = rect.size();
component.hAlign = alignment;
if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
{
if ( !qgsDoubleNear( rotation, 0.0 ) )
{
// get rotated label's center point
double xc = rect.width() / 2.0;
double yc = rect.height() / 2.0;
double angle = -rotation;
double xd = xc * std::cos( angle ) - yc * std::sin( angle );
double yd = xc * std::sin( angle ) + yc * std::cos( angle );
component.center = QPointF( component.origin.x() + xd, component.origin.y() + yd );
}
else
{
component.center = rect.center();
}
switch ( vAlignment )
{
case Qgis::TextVerticalAlignment::Top:
break;
case Qgis::TextVerticalAlignment::VerticalCenter:
component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
break;
case Qgis::TextVerticalAlignment::Bottom:
component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
break;
}
QgsTextRenderer::drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Rectangle );
}
if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
{
return;
}
if ( parts & Qgis::TextComponent::Buffer || parts & Qgis::TextComponent::Text || parts & Qgis::TextComponent::Shadow )
{
drawTextInternal( parts, context, format, component,
document, metrics,
alignment, vAlignment, mode );
}
}
void QgsTextRenderer::drawPart( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
{
const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
const double fontScale = calculateScaleFactorForFormat( context, format );
const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
drawParts( origin, rotation, alignment, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Point );
}
void QgsTextRenderer::drawParts( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents parts, Qgis::TextLayoutMode mode )
{
if ( !context.painter() )
{
return;
}
Component component;
component.dpiRatio = 1.0;
component.origin = origin;
component.rotation = rotation;
component.hAlign = alignment;
if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
{
QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
}
if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
{
return;
}
if ( parts & Qgis::TextComponent::Buffer || parts & Qgis::TextComponent::Text || parts & Qgis::TextComponent::Shadow )
{
drawTextInternal( parts, context, format, component,
document,
metrics,
alignment, Qgis::TextVerticalAlignment::Top,
mode );
}
}
QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
{
return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
}
double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format,
const QgsTextDocumentMetrics &metrics,
Qgis::TextLayoutMode mode )
{
QPainter *p = context.painter();
Qgis::TextOrientation orientation = format.orientation();
if ( format.orientation() == Qgis::TextOrientation::RotationBased )
{
if ( component.rotation >= -315 && component.rotation < -90 )
{
orientation = Qgis::TextOrientation::Vertical;
}
else if ( component.rotation >= -90 && component.rotation < -45 )
{
orientation = Qgis::TextOrientation::Vertical;
}
else
{
orientation = Qgis::TextOrientation::Horizontal;
}
}
QgsTextBufferSettings buffer = format.buffer();
const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage
? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
: context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
const double scaleFactor = calculateScaleFactorForFormat( context, format );
std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
if ( mode == Qgis::TextLayoutMode::Labeling )
{
// label size has already been calculated using any symbology reference scale factor -- we need
// to temporarily remove the reference scale here or we'll be applying the scaling twice
referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
}
if ( metrics.isNullFontSize() )
return 0;
referenceScaleOverride.reset();
QPainterPath path;
path.setFillRule( Qt::WindingFill );
double advance = 0;
double height = component.size.height();
switch ( orientation )
{
case Qgis::TextOrientation::Horizontal:
{
// NOT SUPPORTED BY THIS METHOD ANYMORE -- buffer drawing is handled in drawTextInternalHorizontal since QGIS 3.42
break;
}
case Qgis::TextOrientation::Vertical:
case Qgis::TextOrientation::RotationBased:
{
double partYOffset = component.offset.y() * scaleFactor;
const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
double partLastDescent = 0;
int fragmentIndex = 0;
for ( const QgsTextFragment &fragment : component.block )
{
const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
const QFontMetricsF fragmentMetrics( fragmentFont );
const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
for ( const QString &part : parts )
{
double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
partYOffset += fragmentMetrics.ascent() / scaleFactor;
path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
partYOffset += letterSpacing;
}
partLastDescent = fragmentMetrics.descent() / scaleFactor;
fragmentIndex++;
}
height = partYOffset + partLastDescent;
advance = partYOffset - component.offset.y() * scaleFactor;
break;
}
}
QColor bufferColor = buffer.color();
bufferColor.setAlphaF( buffer.opacity() );
QPen pen( bufferColor );
pen.setWidthF( penSize * scaleFactor );
pen.setJoinStyle( buffer.joinStyle() );
QColor tmpColor( bufferColor );
// honor pref for whether to fill buffer interior
if ( !buffer.fillBufferInterior() )
{
tmpColor.setAlpha( 0 );
}
// store buffer's drawing in QPicture for drop shadow call
QPicture buffPict;
QPainter buffp;
buffp.begin( &buffPict );
if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
{
context.setPainter( &buffp );
std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
tmpEffect->begin( context );
context.painter()->setPen( pen );
context.painter()->setBrush( tmpColor );
if ( scaleFactor != 1.0 )
context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
context.painter()->drawPath( path );
if ( scaleFactor != 1.0 )
context.painter()->scale( scaleFactor, scaleFactor );
tmpEffect->end( context );
context.setPainter( p );
}
else
{
if ( scaleFactor != 1.0 )
buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
buffp.setPen( pen );
buffp.setBrush( tmpColor );
buffp.drawPath( path );
}
buffp.end();
if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer )
{
QgsTextRenderer::Component bufferComponent = component;
bufferComponent.origin = QPointF( 0.0, 0.0 );
bufferComponent.picture = buffPict;
bufferComponent.pictureBuffer = penSize / 2.0;
bufferComponent.size.setHeight( height );
if ( format.orientation() == Qgis::TextOrientation::Vertical || format.orientation() == Qgis::TextOrientation::RotationBased )
{
bufferComponent.offset.setY( - bufferComponent.size.height() );
}
drawShadow( context, bufferComponent, format );
}
QgsScopedQPainterState painterState( p );
context.setPainterFlagsUsingContext( p );
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
p->setCompositionMode( buffer.blendMode() );
}
// scale for any print output or image saving @ specific dpi
p->scale( component.dpiRatio, component.dpiRatio );
QgsPainting::applyScaleFixForQPictureDpi( p );
p->drawPicture( 0, 0, buffPict );
return advance / scaleFactor;
}
void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics,
Qgis::TextLayoutMode mode )
{
QgsTextMaskSettings mask = format.mask();
// the mask is drawn to a side painter
// or to the main painter for preview
QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
if ( ! p )
return;
double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage
? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
: context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
// buffer: draw the text with a big pen
QPainterPath path;
path.setFillRule( Qt::WindingFill );
const double scaleFactor = calculateScaleFactorForFormat( context, format );
// TODO: vertical text mode was ignored when masking feature was added.
// Hopefully Oslandia come back and fix this? Hint hint...
std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
if ( mode == Qgis::TextLayoutMode::Labeling )
{
// label size has already been calculated using any symbology reference scale factor -- we need
// to temporarily remove the reference scale here or we'll be applying the scaling twice
referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
}
if ( metrics.isNullFontSize() )
return;
referenceScaleOverride.reset();
double xOffset = 0;
int fragmentIndex = 0;
for ( const QgsTextFragment &fragment : component.block )
{
if ( !fragment.isWhitespace() && !fragment.isImage() )
{
const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
}
xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
fragmentIndex++;
}
QColor bufferColor( Qt::gray );
bufferColor.setAlphaF( mask.opacity() );
QPen pen;
QBrush brush;
brush.setColor( bufferColor );
pen.setColor( bufferColor );
pen.setWidthF( penSize * scaleFactor );
pen.setJoinStyle( mask.joinStyle() );
QgsScopedQPainterState painterState( p );
context.setPainterFlagsUsingContext( p );
// scale for any print output or image saving @ specific dpi
p->scale( component.dpiRatio, component.dpiRatio );
if ( mask.paintEffect() && mask.paintEffect()->enabled() )
{
QgsPainterSwapper swapper( context, p );
{
QgsEffectPainter effectPainter( context, mask.paintEffect() );
if ( scaleFactor != 1.0 )
context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
context.painter()->setPen( pen );
context.painter()->setBrush( brush );
context.painter()->drawPath( path );
if ( scaleFactor != 1.0 )
context.painter()->scale( scaleFactor, scaleFactor );
}
}
else
{
if ( scaleFactor != 1.0 )
p->scale( 1 / scaleFactor, 1 / scaleFactor );
p->setPen( pen );
p->setBrush( brush );
p->drawPath( path );
if ( scaleFactor != 1.0 )
p->scale( scaleFactor, scaleFactor );
}
}
double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
{
const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( textLines, format );
if ( doc.size() == 0 )
return 0;
return textWidth( context, format, doc );
}
double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
{
//calculate max width of text lines
const double scaleFactor = calculateScaleFactorForFormat( context, format );
const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
// width doesn't change depending on layout mode, we can use anything here
return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
}
double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth )
{
QStringList lines;
for ( const QString &line : textLines )
{
if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
{
lines.append( wrappedText( context, line, maxLineWidth, format ) );
}
else
{
lines.append( line );
}
}
const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( lines, format );
return textHeight( context, format, doc, mode );
}
double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
{
const double scaleFactor = calculateScaleFactorForFormat( context, format );
bool isNullSize = false;
const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
if ( isNullSize )
return 0;
const QFontMetrics fm( baseFont );
const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
if ( !includeEffects )
return height;
double maxExtension = 0;
const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
if ( format.buffer().enabled() )
{
maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
? fontSize * format.buffer().size() / 100
: context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
}
if ( format.shadow().enabled() )
{
maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
? fontSize * format.shadow().offsetDistance() / 100
: context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
)
+ ( format.shadow().blurRadiusUnit() == Qgis::RenderUnit::Percentage
? fontSize * format.shadow().blurRadius() / 100
: context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
);
}
if ( format.background().enabled() )
{
maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
+ context.convertToPainterUnits( format.background().strokeWidth(), format.background().strokeWidthUnit(), format.background().strokeWidthMapUnitScale() ) / 2.0;
if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
{
maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
}
}
return height + maxExtension;
}
bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
{
if ( qgsDoubleNear( width, 0.0 ) )
return false;
const QStringList multiLineSplit = text.split( '\n' );
const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
return currentTextWidth > width;
}
QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
{
const QStringList lines = text.split( '\n' );
QStringList outLines;
for ( const QString &line : lines )
{
if ( textRequiresWrapping( context, line, width, format ) )
{
//first step is to identify words which must be on their own line (too long to fit)
const QStringList words = line.split( ' ' );
QStringList linesToProcess;
QString wordsInCurrentLine;
for ( const QString &word : words )
{
if ( textRequiresWrapping( context, word, width, format ) )
{
//too long to fit
if ( !wordsInCurrentLine.isEmpty() )
linesToProcess << wordsInCurrentLine;
wordsInCurrentLine.clear();
linesToProcess << word;
}
else
{
if ( !wordsInCurrentLine.isEmpty() )
wordsInCurrentLine.append( ' ' );
wordsInCurrentLine.append( word );
}
}
if ( !wordsInCurrentLine.isEmpty() )
linesToProcess << wordsInCurrentLine;
for ( const QString &line : std::as_const( linesToProcess ) )
{
QString remainingText = line;
int lastPos = remainingText.lastIndexOf( ' ' );
while ( lastPos > -1 )
{
//check if remaining text is short enough to go in one line
if ( !textRequiresWrapping( context, remainingText, width, format ) )
{
break;
}
if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
{
outLines << remainingText.left( lastPos );
remainingText = remainingText.mid( lastPos + 1 );
lastPos = 0;
}
lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
}
outLines << remainingText;
}
}
else
{
outLines << line;
}
}
return outLines;
}
double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
{
QgsTextDocument document = doc;
document.applyCapitalization( format.capitalization() );
//calculate max height of text lines
const double scaleFactor = calculateScaleFactorForFormat( context, format );
const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
if ( metrics.isNullFontSize() )
return 0;
return metrics.documentSize( mode, format.orientation() ).height();
}
void QgsTextRenderer::drawBackground( QgsRenderContext &context, const QgsTextRenderer::Component &c, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
{
Component component = c;
QgsTextBackgroundSettings background = format.background();
QPainter *prevP = context.painter();
QPainter *p = context.painter();
std::unique_ptr< QgsPaintEffect > tmpEffect;
if ( background.paintEffect() && background.paintEffect()->enabled() )
{
tmpEffect.reset( background.paintEffect()->clone() );
tmpEffect->begin( context );
p = context.painter();
}
//QgsDebugMsgLevel( QStringLiteral( "Background label rotation: %1" ).arg( component.rotation() ), 4 );
// shared calculations between shapes and SVG
// configure angles, set component rotation and rotationOffset
const double originAdjustRotationRadians = -component.rotation;
if ( background.rotationType() != QgsTextBackgroundSettings::RotationFixed )
{
component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
component.rotationOffset =
background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
}
else // RotationFixed
{
component.rotation = 0.0; // don't use label's rotation
component.rotationOffset = background.rotation();
}
const double scaleFactor = calculateScaleFactorForFormat( context, format );
if ( mode != Qgis::TextLayoutMode::Labeling )
{
// need to calculate size of text
const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
double width = documentSize.width();
double height = documentSize.height();
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
switch ( component.hAlign )
{
case Qgis::TextHorizontalAlignment::Left:
case Qgis::TextHorizontalAlignment::Justify:
component.center = QPointF( component.origin.x() + width / 2.0,
component.origin.y() + height / 2.0 );
break;
case Qgis::TextHorizontalAlignment::Center:
component.center = QPointF( component.origin.x() + component.size.width() / 2.0,
component.origin.y() + height / 2.0 );
break;
case Qgis::TextHorizontalAlignment::Right:
component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0,
component.origin.y() + height / 2.0 );
break;
}
break;
case Qgis::TextLayoutMode::Point:
{
bool isNullSize = false;
QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
switch ( component.hAlign )
{
case Qgis::TextHorizontalAlignment::Left:
case Qgis::TextHorizontalAlignment::Justify:
component.center = QPointF( component.origin.x() + width / 2.0,
component.origin.y() - height / 2.0 + originAdjust );
break;
case Qgis::TextHorizontalAlignment::Center:
component.center = QPointF( component.origin.x(),
component.origin.y() - height / 2.0 + originAdjust );
break;
case Qgis::TextHorizontalAlignment::Right:
component.center = QPointF( component.origin.x() - width / 2.0,
component.origin.y() - height / 2.0 + originAdjust );
break;
}
// apply rotation to center point
if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
{
const double dx = component.center.x() - component.origin.x();
const double dy = component.center.y() - component.origin.y();
component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
}
break;
}
case Qgis::TextLayoutMode::Labeling:
break;
}
if ( format.background().sizeType() != QgsTextBackgroundSettings::SizeFixed )
component.size = QSizeF( width, height );
}
// TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
switch ( background.type() )
{
case QgsTextBackgroundSettings::ShapeSVG:
case QgsTextBackgroundSettings::ShapeMarkerSymbol:
{
// all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
return;
if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
return;
double sizeOut = 0.0;
{
QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
// only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
{
sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
}
else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
{
sizeOut = std::max( component.size.width(), component.size.height() );
double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
// add buffer
sizeOut += bufferSize * 2;
}
}
// don't bother rendering symbols smaller than 1x1 pixels in size
// TODO: add option to not show any svgs under/over a certain size
if ( sizeOut < 1.0 )
return;
std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
{
QVariantMap map; // for SVG symbology marker
map[QStringLiteral( "name" )] = background.svgFile().trimmed();
map[QStringLiteral( "size" )] = QString::number( sizeOut );
map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter
// offset is handled by this local painter
// TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
//map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
//map["offset_unit"] = QgsUnitTypes::encodeUnit(
// tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
map[QStringLiteral( "fill" )] = background.fillColor().name();
map[QStringLiteral( "outline" )] = background.strokeColor().name();
map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() );
map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape )
{
QgsTextShadowSettings shadow = format.shadow();
// configure SVG shadow specs
QVariantMap shdwmap( map );
shdwmap[QStringLiteral( "fill" )] = shadow.color().name();
shdwmap[QStringLiteral( "outline" )] = shadow.color().name();
shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut );
// store SVG's drawing in QPicture for drop shadow call
QPicture svgPict;
QPainter svgp;
svgp.begin( &svgPict );
// draw shadow symbol
// clone current render context map unit/mm conversion factors, but not
// other map canvas parameters, then substitute this painter for use in symbology painting
// NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
// but will be created relative to the SVG's computed size, not the current map canvas
QgsRenderContext shdwContext;
shdwContext.setMapToPixel( context.mapToPixel() );
shdwContext.setScaleFactor( context.scaleFactor() );
shdwContext.setPainter( &svgp );
std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
svgp.end();
component.picture = svgPict;
// TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
component.pictureBuffer = 0.0;
component.size = QSizeF( sizeOut, sizeOut );
component.offset = QPointF( 0.0, 0.0 );
// rotate about origin center of SVG
QgsScopedQPainterState painterState( p );
context.setPainterFlagsUsingContext( p );
p->translate( component.center.x(), component.center.y() );
p->rotate( component.rotation );
double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
p->translate( QPointF( xoff, yoff ) );
p->rotate( component.rotationOffset );
p->translate( -sizeOut / 2, sizeOut / 2 );
drawShadow( context, component, format );
}
renderedSymbol.reset( );
QgsSymbolLayer *symL = QgsSvgMarkerSymbolLayer::create( map );
renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) );
}
else
{
renderedSymbol.reset( background.markerSymbol()->clone() );
renderedSymbol->setSize( sizeOut );
renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
}
renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
// draw the actual symbol
QgsScopedQPainterState painterState( p );
context.setPainterFlagsUsingContext( p );
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
p->setCompositionMode( background.blendMode() );
}
p->translate( component.center.x(), component.center.y() );
p->rotate( component.rotation );
double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
p->translate( QPointF( xoff, yoff ) );
p->rotate( component.rotationOffset );
const QgsFeature f = context.expressionContext().feature();
renderedSymbol->startRender( context, context.expressionContext().fields() );
renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
renderedSymbol->stopRender( context );
p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
break;
}
case QgsTextBackgroundSettings::ShapeRectangle:
case QgsTextBackgroundSettings::ShapeCircle:
case QgsTextBackgroundSettings::ShapeSquare:
case QgsTextBackgroundSettings::ShapeEllipse:
{
double w = component.size.width();
double h = component.size.height();
if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
{
w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
background.sizeMapUnitScale() );
h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
background.sizeMapUnitScale() );
}
else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
{
if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
{
if ( w > h )
h = w;
else if ( h > w )
w = h;
}
else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
{
// start with label bound by circle
h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
w = h;
}
else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
{
// start with label bound by ellipse
h = h * M_SQRT1_2 * 2;
w = w * M_SQRT1_2 * 2;
}
double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
background.sizeMapUnitScale() );
double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
background.sizeMapUnitScale() );
w += bufferWidth * 2;
h += bufferHeight * 2;
}
// offsets match those of symbology: -x = left, -y = up
QRectF rect( -w / 2.0, - h / 2.0, w, h );
if ( rect.isNull() )
return;
QgsScopedQPainterState painterState( p );
context.setPainterFlagsUsingContext( p );
p->translate( QPointF( component.center.x(), component.center.y() ) );
p->rotate( component.rotation );
double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
p->translate( QPointF( xoff, yoff ) );
p->rotate( component.rotationOffset );
QPainterPath path;
// Paths with curves must be enlarged before conversion to QPolygonF, or
// the curves are approximated too much and appear jaggy
QTransform t = QTransform::fromScale( 10, 10 );
// inverse transform used to scale created polygons back to expected size
QTransform ti = t.inverted();
if ( background.type() == QgsTextBackgroundSettings::ShapeRectangle
|| background.type() == QgsTextBackgroundSettings::ShapeSquare )
{
if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
{
path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
}
else
{
const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
path.addRoundedRect( rect, xRadius, yRadius );
}
}
else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse
|| background.type() == QgsTextBackgroundSettings::ShapeCircle )
{
path.addEllipse( rect );
}
QPolygonF tempPolygon = path.toFillPolygon( t );
QPolygonF polygon = ti.map( tempPolygon );
QPicture shapePict;
QPainter *oldp = context.painter();
QPainter shapep;
shapep.begin( &shapePict );
context.setPainter( &shapep );
std::unique_ptr< QgsFillSymbol > renderedSymbol;
renderedSymbol.reset( background.fillSymbol()->clone() );
renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
const QgsFeature f = context.expressionContext().feature();
renderedSymbol->startRender( context, context.expressionContext().fields() );
renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
renderedSymbol->stopRender( context );
shapep.end();
context.setPainter( oldp );
if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape )
{
component.picture = shapePict;
component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
component.size = rect.size();
component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
drawShadow( context, component, format );
}
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
p->setCompositionMode( background.blendMode() );
}
// scale for any print output or image saving @ specific dpi
p->scale( component.dpiRatio, component.dpiRatio );
QgsPainting::applyScaleFixForQPictureDpi( p );
p->drawPicture( 0, 0, shapePict );
p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
break;
}
}
if ( tmpEffect )
{
tmpEffect->end( context );
context.setPainter( prevP );
}
}
void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
{
QgsTextShadowSettings shadow = format.shadow();
QPainter *p = context.painter();
const double componentWidth = component.size.width();
const double componentHeight = component.size.height();
const double xOffset = component.offset.x();
const double yOffset = component.offset.y();
double pictbuffer = component.pictureBuffer;
// generate pixmap representation of label component drawing
bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage
? fontSize * shadow.blurRadius() / 100
: context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
radius = static_cast< int >( radius + 0.5 ); //NOLINT
// TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
// to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
double blurBufferClippingScale = 3.75;
int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
QImage::Format_ARGB32_Premultiplied );
// TODO: add labeling gui option to not show any shadows under/over a certain size
// keep very small QImages from causing paint device issues, i.e. must be at least > 1
int minBlurImgSize = 1;
// max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
// 4 x QgsSvgCache limit for output to print/image at higher dpi
// TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
int maxBlurImgSize = 40000;
if ( blurImg.isNull()
|| ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize )
|| ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
return;
blurImg.fill( QColor( Qt::transparent ).rgba() );
QPainter pictp;
if ( !pictp.begin( &blurImg ) )
return;
pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
QPointF imgOffset( blurbuffer + pictbuffer + xOffset,
blurbuffer + pictbuffer + componentHeight + yOffset );
pictp.drawPicture( imgOffset,
component.picture );
// overlay shadow color
pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
pictp.fillRect( blurImg.rect(), shadow.color() );
pictp.end();
// blur the QImage in-place
if ( shadow.blurRadius() > 0.0 && radius > 0 )
{
QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
}
#if 0
// debug rect for QImage shadow registration and clipping visualization
QPainter picti;
picti.begin( &blurImg );
picti.setBrush( Qt::Dense7Pattern );
QPen imgPen( QColor( 0, 0, 255, 255 ) );
imgPen.setWidth( 1 );
picti.setPen( imgPen );
picti.setOpacity( 0.1 );
picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
picti.end();
#endif
const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage
? fontSize * shadow.offsetDistance() / 100
: context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
if ( shadow.offsetGlobal() )
{
// TODO: check for differences in rotation origin and cw/ccw direction,
// when this shadow function is used for something other than labels
// it's 0-->cw-->360 for labels
//QgsDebugMsgLevel( QStringLiteral( "Shadow aggregated label rotation (degrees): %1" ).arg( component.rotation() + component.rotationOffset() ), 4 );
angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
}
QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ),
-offsetDist * std::sin( angleRad + M_PI_2 ) );
p->save();
context.setPainterFlagsUsingContext( p );
// this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
// corresponding flag in the render context instead...
p->setRenderHint( QPainter::SmoothPixmapTransform );
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
p->setCompositionMode( shadow.blendMode() );
}
p->setOpacity( shadow.opacity() );
double scale = shadow.scale() / 100.0;
// TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
p->scale( scale, scale );
if ( component.useOrigin )
{
p->translate( component.origin.x(), component.origin.y() );
}
p->translate( transPt );
p->translate( -imgOffset.x(),
-imgOffset.y() );
p->drawImage( 0, 0, blurImg );
p->restore();
// debug rects
#if 0
// draw debug rect for QImage painting registration
p->save();
p->setBrush( Qt::NoBrush );
QPen imgPen( QColor( 255, 0, 0, 10 ) );
imgPen.setWidth( 2 );
imgPen.setStyle( Qt::DashLine );
p->setPen( imgPen );
p->scale( scale, scale );
if ( component.useOrigin() )
{
p->translate( component.origin().x(), component.origin().y() );
}
p->translate( transPt );
p->translate( -imgOffset.x(),
-imgOffset.y() );
p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
p->restore();
// draw debug rect for passed in component dimensions
p->save();
p->setBrush( Qt::NoBrush );
QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
componentRectPen.setWidth( 1 );
if ( component.useOrigin() )
{
p->translate( component.origin().x(), component.origin().y() );
}
p->setPen( componentRectPen );
p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
p->restore();
#endif
}
void QgsTextRenderer::drawTextInternal( Qgis::TextComponents components,
QgsRenderContext &context,
const QgsTextFormat &format,
const Component &component,
const QgsTextDocument &document,
const QgsTextDocumentMetrics &metrics,
Qgis::TextHorizontalAlignment alignment, Qgis::TextVerticalAlignment vAlignment, Qgis::TextLayoutMode mode )
{
if ( !context.painter() )
{
return;
}
const double fontScale = calculateScaleFactorForFormat( context, format );
std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
if ( mode == Qgis::TextLayoutMode::Labeling )
{
// label size has already been calculated using any symbology reference scale factor -- we need
// to temporarily remove the reference scale here or we'll be applying the scaling twice
referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
}
if ( metrics.isNullFontSize() )
return;
referenceScaleOverride.reset();
double rotation = 0;
const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
switch ( orientation )
{
case Qgis::TextOrientation::Horizontal:
{
drawTextInternalHorizontal( context, format, components, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
break;
}
case Qgis::TextOrientation::Vertical:
case Qgis::TextOrientation::RotationBased:
{
// TODO: vertical text renderer currently doesn't handle one-pass buffer + text drawing
if ( components & Qgis::TextComponent::Buffer )
drawTextInternalVertical( context, format, Qgis::TextComponent::Buffer, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
if ( components & Qgis::TextComponent::Text )
drawTextInternalVertical( context, format, Qgis::TextComponent::Text, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
break;
}
}
}
Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
{
rotation = -component.rotation * 180 / M_PI;
switch ( format.orientation() )
{
case Qgis::TextOrientation::RotationBased:
{
// Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
if ( rotation >= -315 && rotation < -90 )
{
rotation -= 90;
return Qgis::TextOrientation::Vertical;
}
else if ( rotation >= -90 && rotation < -45 )
{
rotation += 90;
return Qgis::TextOrientation::Vertical;
}
return Qgis::TextOrientation::Horizontal;
}
case Qgis::TextOrientation::Horizontal:
case Qgis::TextOrientation::Vertical:
return format.orientation();
}
return Qgis::TextOrientation::Horizontal;
}
void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
{
const QString blockText = block.toPlainText();
QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
finder.toStart();
int wordBoundaries = 0;
while ( finder.toNextBoundary() != -1 )
{
if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
wordBoundaries++;
}
if ( wordBoundaries > 0 )
{
// word boundaries found => justify by padding word spacing
extraWordSpace = spaceToDistribute / wordBoundaries;
}
else
{
// no word boundaries found => justify by letter spacing
QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
finder.toStart();
int graphemeBoundaries = 0;
while ( finder.toNextBoundary() != -1 )
{
if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
graphemeBoundaries++;
}
if ( graphemeBoundaries > 0 )
{
extraLetterSpace = spaceToDistribute / graphemeBoundaries;
}
}
}
void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
{
const double prevWordSpace = font.wordSpacing();
font.setWordSpacing( prevWordSpace + extraWordSpace );
const double prevLetterSpace = font.letterSpacing();
font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
}
void QgsTextRenderer::renderBlockHorizontal( const QgsTextBlock &block, int blockIndex,
const QgsTextDocumentMetrics &metrics, QgsRenderContext &context,
const QgsTextFormat &format,
QPainter *painter, bool forceRenderAsPaths,
double fontScale, double extraWordSpace, double extraLetterSpace,
Qgis::TextLayoutMode mode, DeferredRenderBlock *deferredRenderBlock )
{
if ( !metrics.isNullFontSize() )
{
double xOffset = 0;
int fragmentIndex = 0;
for ( const QgsTextFragment &fragment : block )
{
// draw text, QPainterPath method
if ( !fragment.isWhitespace() && !fragment.isImage() )
{
QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
if ( !qgsDoubleNear( extraWordSpace, 0 ) || !qgsDoubleNear( extraLetterSpace, 0 ) )
applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
if ( deferredRenderBlock )
{
DeferredRenderFragment renderFragment;
renderFragment.color = textColor;
if ( forceRenderAsPaths )
{
renderFragment.path.setFillRule( Qt::WindingFill );
renderFragment.path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
}
renderFragment.font = fragmentFont;
renderFragment.point = QPointF( xOffset, yOffset );
renderFragment.text = fragment.text();
deferredRenderBlock->fragments.append( renderFragment );
}
else if ( forceRenderAsPaths )
{
painter->setBrush( textColor );
QPainterPath path;
path.setFillRule( Qt::WindingFill );
path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
painter->drawPath( path );
}
else
{
painter->setPen( textColor );
painter->setFont( fragmentFont );
painter->drawText( QPointF( xOffset, yOffset ), fragment.text() );
}
}
else if ( fragment.isImage() )
{
bool fitsInCache = false;
const double imageWidth = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
const double imageHeight = metrics.fragmentFixedHeight( blockIndex, fragmentIndex, mode ) * fontScale;
const QImage image = QgsApplication::imageCache()->pathAsImage( fragment.characterFormat().imagePath(),
QSize( static_cast< int >( std::round( imageWidth ) ),
static_cast< int >( std::round( imageHeight ) ) ),
false,
1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
const double imageBaseline = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
const double yOffset = imageBaseline - image.height();
if ( !image.isNull() )
painter->drawImage( QPointF( xOffset, yOffset ), image );
}
xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
fragmentIndex ++;
}
}
};
bool QgsTextRenderer::usePathsToRender( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
{
switch ( context.textRenderFormat() )
{
case Qgis::TextRenderFormat::AlwaysOutlines:
return true;
case Qgis::TextRenderFormat::AlwaysText:
return false;
case Qgis::TextRenderFormat::PreferText:
{
// Prefer not to use paths -- but certain conditions will require us to use them
if ( format.buffer().enabled() )
{
// text buffer requires use of paths
// TODO: this was the original cause of use switching from text to paths by default,
// but that was way back in the 2.0 days and maybe the Qt issues have now been fixed?
return true;
}
// underline/overline/strikethrough looks different between path/non-path renders.
// TODO: validate which is correct. For now, maintain default appearance from before this code
// was introduced
if ( format.font().underline()
|| format.font().overline()
|| format.font().strikeOut()
|| std::any_of( document.begin(), document.end(), []( const QgsTextBlock & block )
{
return std::any_of( block.begin(), block.end(), []( const QgsTextFragment & fragment )
{
return fragment.characterFormat().underline() == QgsTextCharacterFormat::BooleanValue::SetTrue
|| fragment.characterFormat().overline() == QgsTextCharacterFormat::BooleanValue::SetTrue
|| fragment.characterFormat().strikeOut() == QgsTextCharacterFormat::BooleanValue::SetTrue;
} );
} ) )
return true;
return false;
}
}
BUILTIN_UNREACHABLE
}
bool QgsTextRenderer::usePictureToRender( const QgsRenderContext &, const QgsTextFormat &, const QgsTextDocument &document )
{
return std::any_of( document.begin(), document.end(), []( const QgsTextBlock & block )
{
return std::any_of( block.begin(), block.end(), []( const QgsTextFragment & fragment )
{
return fragment.isImage();
} );
} );
}
QVector< QgsTextRenderer::BlockMetrics > QgsTextRenderer::calculateBlockMetrics( const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode, double targetWidth, const Qgis::TextHorizontalAlignment hAlignment )
{
QVector< BlockMetrics > blockMetrics;
blockMetrics.reserve( document.size() );
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
|| document.size() > 1 );
const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 )
|| document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
BlockMetrics thisBlockMetrics;
// figure x offset for horizontal alignment of multiple lines
thisBlockMetrics.width = metrics.blockWidth( blockIndex );
if ( adjustForAlignment )
{
double blockWidthDiff = 0;
switch ( blockAlignment )
{
case Qgis::TextHorizontalAlignment::Center:
blockWidthDiff = ( targetWidth - thisBlockMetrics.width - metrics.blockLeftMargin( blockIndex ) - metrics.blockRightMargin( blockIndex ) ) * 0.5 + metrics.blockLeftMargin( blockIndex );
break;
case Qgis::TextHorizontalAlignment::Right:
blockWidthDiff = targetWidth - thisBlockMetrics.width - metrics.blockRightMargin( blockIndex );
break;
case Qgis::TextHorizontalAlignment::Justify:
if ( !isFinalLineInParagraph && targetWidth > thisBlockMetrics.width )
{
calculateExtraSpacingForLineJustification( targetWidth - thisBlockMetrics.width, block, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace );
thisBlockMetrics.width = targetWidth;
}
blockWidthDiff = metrics.blockLeftMargin( blockIndex );
break;
case Qgis::TextHorizontalAlignment::Left:
blockWidthDiff = metrics.blockLeftMargin( blockIndex );
break;
}
switch ( mode )
{
case Qgis::TextLayoutMode::Labeling:
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
thisBlockMetrics.xOffset = blockWidthDiff;
break;
case Qgis::TextLayoutMode::Point:
{
switch ( blockAlignment )
{
case Qgis::TextHorizontalAlignment::Right:
thisBlockMetrics.xOffset = blockWidthDiff - targetWidth;
break;
case Qgis::TextHorizontalAlignment::Center:
thisBlockMetrics.xOffset = blockWidthDiff - targetWidth / 2.0;
break;
case Qgis::TextHorizontalAlignment::Left:
case Qgis::TextHorizontalAlignment::Justify:
thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
break;
}
}
break;
}
}
else if ( blockAlignment == Qgis::TextHorizontalAlignment::Left || blockAlignment == Qgis::TextHorizontalAlignment::Justify )
{
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 )
{
QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
double targetWidth = 0.0;
switch ( mode )
{
case Qgis::TextLayoutMode::Labeling:
case Qgis::TextLayoutMode::Point:
targetWidth = documentSize.width();
break;
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
targetWidth = component.size.width();
break;
}
double verticalAlignOffset = 0;
if ( mode == Qgis::TextLayoutMode::Rectangle )
{
const double overallHeight = documentSize.height();
switch ( vAlignment )
{
case Qgis::TextVerticalAlignment::Top:
verticalAlignOffset = metrics.blockVerticalMargin( - 1 );
break;
case Qgis::TextVerticalAlignment::VerticalCenter:
verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5 + metrics.blockVerticalMargin( - 1 );
break;
case Qgis::TextVerticalAlignment::Bottom:
verticalAlignOffset = ( component.size.height() - overallHeight ) + metrics.blockVerticalMargin( - 1 );
break;
}
}
else if ( mode == Qgis::TextLayoutMode::Point )
{
verticalAlignOffset = - metrics.blockVerticalMargin( document.size() - 1 );
}
// should we use text or paths for this render?
const bool usePathsForText = usePathsToRender( context, format, document );
// TODO -- maybe we can avoid the nested vector? Need to confirm whether painter rotation & translation can be
// done ONCE only, upfront
std::unique_ptr< std::vector< DeferredRenderBlock > > deferredBlocks;
// Depending on format settings, we may need to render in multiple passes. Eg buffer than text, or shadow than text.
// We try to avoid this if possible as it requires more work, and just do a single pass, rendering text directly as we go.
// If we need to do multi-pass rendering then we'll calculate paths ONCE upfront and defer actually rendering these.
const bool requiresMultiPassRendering = ( components & Qgis::TextComponent::Buffer && format.buffer().enabled() )
|| ( components & Qgis::TextComponent::Shadow && format.shadow().enabled() && ( format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText || format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer ) );
if ( requiresMultiPassRendering )
{
deferredBlocks = std::make_unique< std::vector< DeferredRenderBlock > >();
deferredBlocks->reserve( document.size() );
}
if ( ( components & Qgis::TextComponent::Buffer )
|| ( components & Qgis::TextComponent::Text )
|| ( 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 )
{
const double blockHeight = metrics.blockHeight( blockIndex );
DeferredRenderBlock *deferredBlock = nullptr;
if ( requiresMultiPassRendering && deferredBlocks )
{
deferredBlocks->emplace_back( DeferredRenderBlock() );
deferredBlock = &deferredBlocks->back();
deferredBlock->fragments.reserve( block.size() );
}
QgsScopedQPainterState painterState( context.painter() );
context.setPainterFlagsUsingContext();
context.painter()->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
context.painter()->rotate( rotation );
// apply to the mask painter the same transformations
if ( maskPainter )
{
maskPainter->save();
maskPainter->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
maskPainter->rotate( rotation );
}
const BlockMetrics thisBlockMetrics = blockMetrics[ blockIndex ];
const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
const QPointF blockOrigin( thisBlockMetrics.xOffset, baseLineOffset + verticalAlignOffset );
if ( deferredBlock )
deferredBlock->origin = blockOrigin;
else
context.painter()->translate( blockOrigin );
if ( maskPainter )
maskPainter->translate( blockOrigin );
Component subComponent;
subComponent.block = block;
subComponent.blockIndex = blockIndex;
subComponent.size = QSizeF( thisBlockMetrics.width, blockHeight );
subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
subComponent.rotation = -component.rotation * 180 / M_PI;
subComponent.rotationOffset = 0.0;
subComponent.extraWordSpacing = thisBlockMetrics.extraWordSpace * fontScale;
subComponent.extraLetterSpacing = thisBlockMetrics.extraLetterSpace * fontScale;
if ( deferredBlock )
deferredBlock->component = subComponent;
// draw the mask below the text (for preview)
if ( format.mask().enabled() )
{
QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
}
// if we are drawing both text + buffer, we'll need a path, as we HAVE to render buffers using paths
const bool needsPaths = usePathsForText
|| ( ( components & Qgis::TextComponent::Buffer ) && format.buffer().enabled() )
|| ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() );
std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
if ( mode == Qgis::TextLayoutMode::Labeling )
{
// label size has already been calculated using any symbology reference scale factor -- we need
// to temporarily remove the reference scale here or we'll be applying the scaling twice
referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
}
referenceScaleOverride.reset();
// now render the actual text
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
context.painter()->setCompositionMode( format.blendMode() );
}
// scale for any print output or image saving @ specific dpi
context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
context.painter()->scale( 1 / fontScale, 1 / fontScale );
context.painter()->setPen( Qt::NoPen );
context.painter()->setBrush( Qt::NoBrush );
renderBlockHorizontal( block, blockIndex, metrics, context, format, context.painter(), needsPaths,
fontScale, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace, mode, deferredBlock );
if ( maskPainter )
maskPainter->restore();
blockIndex++;
}
}
if ( deferredBlocks )
{
renderDeferredBlocks(
context, format, components, *deferredBlocks, usePathsForText, fontScale, component, rotation
);
}
}
void QgsTextRenderer::renderDeferredBlocks( QgsRenderContext &context,
const QgsTextFormat &format,
Qgis::TextComponents components,
const std::vector< DeferredRenderBlock > &deferredBlocks,
bool usePathsForText,
double fontScale,
const Component &component,
double rotation )
{
if ( format.buffer().enabled() && ( components & Qgis::TextComponent::Buffer ) )
{
renderDeferredBuffer( context, format, components, deferredBlocks, fontScale, component, rotation );
}
if ( ( components & Qgis::TextComponent::Shadow )
&& format.shadow().enabled()
&& format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
{
renderDeferredShadowForText( context, format, deferredBlocks, fontScale, component, rotation );
// TODO: there's an optimisation opportunity here -- if we are ALSO rendering the text component,
// we could move the actual text rendering into renderDeferredShadowForText and use the same
// QPicture as we used for the shadow. But we'd need to ensure that all the settings
// which control whether text is rendered as text or paths also also considered.
}
if ( components & Qgis::TextComponent::Text )
{
renderDeferredText( context, deferredBlocks, usePathsForText, fontScale, component, rotation );
}
}
void QgsTextRenderer::renderDeferredShadowForText( QgsRenderContext &context,
const QgsTextFormat &format,
const std::vector< DeferredRenderBlock > &deferredBlocks,
double fontScale,
const Component &component,
double rotation )
{
QgsScopedQPainterState painterState( context.painter() );
context.setPainterFlagsUsingContext();
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 DeferredRenderBlock &block : deferredBlocks )
{
Component subComponent = block.component;
QPainter painter( &subComponent.picture );
painter.setPen( Qt::NoPen );
painter.setBrush( Qt::NoBrush );
painter.scale( 1 / fontScale, 1 / fontScale );
for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
{
if ( !fragment.path.isEmpty() )
{
painter.setBrush( fragment.color );
painter.drawPath( fragment.path );
}
else
{
painter.setPen( fragment.color );
painter.setFont( fragment.font );
painter.drawText( fragment.point, fragment.text );
}
}
painter.end();
subComponent.pictureBuffer = 1.0; // no pen width to deal with, but we'll add 1 px for antialiasing
subComponent.origin = QPointF( 0.0, 0.0 );
const QRectF pictureBoundingRect = subComponent.picture.boundingRect();
subComponent.size = pictureBoundingRect.size();
subComponent.offset = QPointF( -pictureBoundingRect.left(), -pictureBoundingRect.height() - pictureBoundingRect.top() );
context.painter()->translate( block.origin );
drawShadow( context, subComponent, format );
context.painter()->translate( -block.origin );
}
}
void QgsTextRenderer::renderDeferredBuffer( QgsRenderContext &context,
const QgsTextFormat &format,
Qgis::TextComponents components,
const std::vector< DeferredRenderBlock > &deferredBlocks,
double fontScale,
const Component &component,
double rotation )
{
QgsScopedQPainterState painterState( context.painter() );
context.setPainterFlagsUsingContext();
// do we need a drop shadow effect on the buffer component? If so, we'll render the buffer to a QPicture first and then use this
// to generate the shadow, and then render the QPicture as the buffer on top. If not, avoid the unwanted expense of the temporary QPicture
// and render directly.
const bool needsShadowOnBuffer = ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer );
std::unique_ptr< QPicture > bufferPicture;
std::unique_ptr< QPainter > bufferPainter;
QPainter *prevPainter = context.painter();
if ( needsShadowOnBuffer )
{
bufferPicture = std::make_unique< QPicture >();
bufferPainter = std::make_unique< QPainter >( bufferPicture.get() );
context.setPainter( bufferPainter.get() );
}
std::unique_ptr< QgsPaintEffect > tmpEffect;
if ( format.buffer().paintEffect() && format.buffer().paintEffect()->enabled() )
{
tmpEffect.reset( format.buffer().paintEffect()->clone() );
tmpEffect->begin( context );
}
QColor bufferColor = format.buffer().color();
bufferColor.setAlphaF( format.buffer().opacity() );
QPen pen( bufferColor );
const QgsTextBufferSettings &buffer = format.buffer();
const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage
? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
: context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
pen.setWidthF( penSize * fontScale );
pen.setJoinStyle( buffer.joinStyle() );
context.painter()->setPen( pen );
// honor pref for whether to fill buffer interior
if ( !buffer.fillBufferInterior() )
{
bufferColor.setAlpha( 0 );
}
context.painter()->setBrush( bufferColor );
context.painter()->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
context.painter()->rotate( rotation );
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
context.painter()->setCompositionMode( format.buffer().blendMode() );
}
for ( const DeferredRenderBlock &block : deferredBlocks )
{
context.painter()->translate( block.origin );
context.painter()->scale( 1 / fontScale, 1 / fontScale );
for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
{
context.painter()->drawPath( fragment.path );
}
context.painter()->scale( fontScale, fontScale );
context.painter()->translate( -block.origin );
}
if ( tmpEffect )
{
tmpEffect->end( context );
}
if ( needsShadowOnBuffer && bufferPicture )
{
bufferPainter->end();
bufferPainter.reset();
context.setPainter( prevPainter );
QgsTextRenderer::Component bufferComponent = component;
bufferComponent.origin = QPointF( 0.0, 0.0 );
bufferComponent.picture = *bufferPicture;
bufferComponent.pictureBuffer = penSize / 2.0;
const QRectF bufferBoundingBox = bufferPicture->boundingRect();
bufferComponent.size = bufferBoundingBox.size();
bufferComponent.offset = QPointF( -bufferBoundingBox.left(), -bufferBoundingBox.height() - bufferBoundingBox.top() );
drawShadow( context, bufferComponent, format );
// also draw buffer
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
context.painter()->setCompositionMode( buffer.blendMode() );
}
// scale for any print output or image saving @ specific dpi
context.painter()->scale( component.dpiRatio, component.dpiRatio );
QgsPainting::drawPicture( context.painter(), QPointF( 0, 0 ), *bufferPicture );
}
}
void QgsTextRenderer::renderDeferredText( QgsRenderContext &context,
const std::vector< DeferredRenderBlock > &deferredBlocks,
bool usePathsForText,
double fontScale,
const Component &component,
double rotation )
{
QgsScopedQPainterState painterState( context.painter() );
context.setPainterFlagsUsingContext();
context.painter()->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
context.painter()->rotate( rotation );
context.painter()->setPen( Qt::NoPen );
context.painter()->setBrush( Qt::NoBrush );
// draw the text
for ( const DeferredRenderBlock &block : deferredBlocks )
{
context.painter()->translate( block.origin );
context.painter()->scale( 1 / fontScale, 1 / fontScale );
for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
{
if ( usePathsForText )
{
context.painter()->setBrush( fragment.color );
context.painter()->drawPath( fragment.path );
}
else
{
context.painter()->setPen( fragment.color );
context.painter()->setFont( fragment.font );
context.painter()->drawText( fragment.point, fragment.text );
}
}
context.painter()->scale( fontScale, fontScale );
context.painter()->translate( -block.origin );
}
}
void QgsTextRenderer::drawTextInternalVertical( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, Qgis::TextLayoutMode mode, const QgsTextRenderer::Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, Qgis::TextHorizontalAlignment hAlignment, Qgis::TextVerticalAlignment, double rotation )
{
QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
const QStringList textLines = document.toPlainText();
std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
if ( mode == Qgis::TextLayoutMode::Labeling )
{
// label size has already been calculated using any symbology reference scale factor -- we need
// to temporarily remove the reference scale here or we'll be applying the scaling twice
referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
}
if ( metrics.isNullFontSize() )
return;
referenceScaleOverride.reset();
const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
const double actualTextWidth = documentSize.width();
double textRectWidth = 0.0;
switch ( mode )
{
case Qgis::TextLayoutMode::Labeling:
case Qgis::TextLayoutMode::Point:
textRectWidth = actualTextWidth;
break;
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
textRectWidth = component.size.width();
break;
}
int maxLineLength = 0;
for ( const QString &line : std::as_const( textLines ) )
{
maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
}
const double actualLabelHeight = documentSize.height();
int blockIndex = 0;
bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
for ( const QgsTextBlock &block : document )
{
QgsScopedQPainterState painterState( context.painter() );
context.setPainterFlagsUsingContext();
context.painter()->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
context.painter()->rotate( rotation );
// apply to the mask painter the same transformations
if ( maskPainter )
{
maskPainter->save();
maskPainter->translate( component.origin );
if ( !qgsDoubleNear( rotation, 0.0 ) )
maskPainter->rotate( rotation );
}
const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
// figure x offset of multiple lines
double xOffset = metrics.verticalOrientationXOffset( blockIndex );
if ( adjustForAlignment )
{
double hAlignmentOffset = 0;
switch ( hAlignment )
{
case Qgis::TextHorizontalAlignment::Center:
hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
break;
case Qgis::TextHorizontalAlignment::Right:
hAlignmentOffset = textRectWidth - actualTextWidth;
break;
case Qgis::TextHorizontalAlignment::Left:
case Qgis::TextHorizontalAlignment::Justify:
break;
}
switch ( mode )
{
case Qgis::TextLayoutMode::Labeling:
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
xOffset += hAlignmentOffset;
break;
case Qgis::TextLayoutMode::Point:
break;
}
}
double yOffset = 0.0;
switch ( mode )
{
case Qgis::TextLayoutMode::Labeling:
if ( format.orientation() == Qgis::TextOrientation::RotationBased )
{
if ( rotation >= -405 && rotation < -180 )
{
yOffset = 0;
}
else if ( rotation >= 0 && rotation < 45 )
{
xOffset -= actualTextWidth;
yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
}
}
else
{
yOffset = -actualLabelHeight;
}
break;
case Qgis::TextLayoutMode::Point:
yOffset = -actualLabelHeight;
break;
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
yOffset = 0;
break;
}
context.painter()->translate( QPointF( xOffset, yOffset ) );
double currentBlockYOffset = 0;
int fragmentIndex = 0;
for ( const QgsTextFragment &fragment : block )
{
QgsScopedQPainterState fragmentPainterState( context.painter() );
// apply some character replacement to draw symbols in vertical presentation
const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
QFontMetricsF fragmentMetrics( fragmentFont );
const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
Component subComponent;
subComponent.block = QgsTextBlock( fragment );
subComponent.blockIndex = blockIndex;
subComponent.firstFragmentIndex = fragmentIndex;
subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
subComponent.offset = QPointF( 0.0, currentBlockYOffset );
subComponent.rotation = -component.rotation * 180 / M_PI;
subComponent.rotationOffset = 0.0;
// draw the mask below the text (for preview)
if ( format.mask().enabled() )
{
// WARNING: totally broken! (has been since mask was introduced)
#if 0
QgsTextRenderer::drawMask( context, subComponent, format );
#endif
}
if ( components & Qgis::TextComponent::Buffer )
{
currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
}
if ( ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
{
// draw text, QPainterPath method
QPainterPath path;
path.setFillRule( Qt::WindingFill );
const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
double partYOffset = 0.0;
for ( const QString &part : parts )
{
double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
partYOffset += fragmentMetrics.ascent() / fontScale;
path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
partYOffset += letterSpacing;
}
// store text's drawing in QPicture for drop shadow call
QPicture textPict;
QPainter textp;
textp.begin( &textPict );
textp.setPen( Qt::NoPen );
QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
textp.setBrush( textColor );
textp.scale( 1 / fontScale, 1 / fontScale );
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() );
textp.end();
if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
{
subComponent.picture = textPict;
subComponent.pictureBuffer = 0.0; // no pen width to deal with
subComponent.origin = QPointF( 0.0, currentBlockYOffset );
const double prevY = subComponent.offset.y();
subComponent.offset = QPointF( 0, -subComponent.size.height() );
subComponent.useOrigin = true;
QgsTextRenderer::drawShadow( context, subComponent, format );
subComponent.useOrigin = false;
subComponent.offset = QPointF( 0, prevY );
}
// paint the text
if ( context.rasterizedRenderingPolicy() != Qgis::RasterizedRenderingPolicy::ForceVector )
{
context.painter()->setCompositionMode( format.blendMode() );
}
// scale for any print output or image saving @ specific dpi
context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
// TODO -- this should respect the context's TextRenderFormat
// draw outlined text
context.painter()->translate( 0, currentBlockYOffset );
QgsPainting::applyScaleFixForQPictureDpi( context.painter() );
context.painter()->drawPicture( 0, 0, textPict );
currentBlockYOffset += partYOffset;
}
fragmentIndex++;
}
if ( maskPainter )
maskPainter->restore();
blockIndex++;
}
}
double QgsTextRenderer::calculateScaleFactorForFormat( const QgsRenderContext &context, const QgsTextFormat &format )
{
if ( !( context.flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ) )
return 1.0;
const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
// THESE THRESHOLDS MAY NEED TWEAKING!
// NOLINTBEGIN(bugprone-branch-clone)
// for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
// we scale the painter up so that we render small text at 200 pixel size and let the painter scaling handle making it the correct size
if ( pixelSize < 50 )
return 200 / pixelSize;
//... but for large font sizes we might run into https://bugreports.qt.io/browse/QTBUG-98778, which messes up the spacing between words for large fonts!
// so instead we scale down the painter so that we render the text at 200 pixel size and let painter scaling handle making it the correct size
else if ( pixelSize > 200 )
return 200 / pixelSize;
else
return 1.0;
// NOLINTEND(bugprone-branch-clone)
}