QGIS API Documentation 3.41.0-Master (3440c17df1d)
Loading...
Searching...
No Matches
qgstextrenderer.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstextrenderer.cpp
3 -------------------
4 begin : September 2015
5 copyright : (C) Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgstextrenderer.h"
17#include "qgstextformat.h"
18#include "qgstextdocument.h"
20#include "qgstextfragment.h"
21#include "qgspallabeling.h"
22#include "qgspainteffect.h"
23#include "qgspainterswapper.h"
25#include "qgssymbollayerutils.h"
26#include "qgsmarkersymbol.h"
27#include "qgsfillsymbol.h"
28#include "qgsunittypes.h"
29#include "qgstextmetrics.h"
31#include "qgsgeos.h"
32#include "qgspainting.h"
33#include "qgsapplication.h"
34#include "qgsimagecache.h"
35#include <optional>
36
37#include <QTextBoundaryFinder>
38
39
41{
42 if ( alignment & Qt::AlignLeft )
44 else if ( alignment & Qt::AlignRight )
46 else if ( alignment & Qt::AlignHCenter )
48 else if ( alignment & Qt::AlignJustify )
50
51 // not supported?
53}
54
56{
57 if ( alignment & Qt::AlignTop )
59 else if ( alignment & Qt::AlignBottom )
61 else if ( alignment & Qt::AlignVCenter )
63 //not supported
64 else if ( alignment & Qt::AlignBaseline )
66
68}
69
70int QgsTextRenderer::sizeToPixel( double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale )
71{
72 return static_cast< int >( c.convertToPainterUnits( size, unit, mapUnitScale ) + 0.5 ); //NOLINT
73}
74
75void 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,
77{
78 QgsTextFormat lFormat = _format;
79 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use format instead of tmpFormat here, it's const and potentially avoids a detach
80 lFormat.updateDataDefinedProperties( context );
81
82 // DO NOT USE _format in the following code, always use lFormat!!
83 QgsTextDocumentRenderContext documentContext;
84 documentContext.setFlags( flags );
85 documentContext.setMaximumWidth( rect.width() );
86
87 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( text, lFormat );
88
89 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
90 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale, documentContext );
91
92 drawDocument( rect, lFormat, metrics.document(), metrics, context, alignment, vAlignment, rotation, mode, flags );
93}
94
95void 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 )
96{
97 const QgsTextFormat tmpFormat = updateShadowPosition( format );
98
100 if ( tmpFormat.background().enabled() )
101 {
103 }
104
105 if ( tmpFormat.shadow().enabled() )
106 {
107 components |= Qgis::TextComponent::Shadow;
108 }
109
110 if ( tmpFormat.buffer().enabled() )
111 {
112 components |= Qgis::TextComponent::Buffer;
113 }
114
115 drawParts( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, components, mode );
116}
117
118void QgsTextRenderer::drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &_format, bool )
119{
120 QgsTextFormat lFormat = _format;
121 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
122 lFormat.updateDataDefinedProperties( context );
123 lFormat = updateShadowPosition( lFormat );
124
125 // DO NOT USE _format in the following code, always use lFormat!!
126 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, lFormat );
127 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
128 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
129
130 drawDocument( point, lFormat, metrics.document(), metrics, context, alignment, rotation );
131}
132
133void QgsTextRenderer::drawDocument( QPointF point, const QgsTextFormat &format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment alignment, double rotation )
134{
136 if ( format.background().enabled() )
137 {
139 }
140
141 if ( format.shadow().enabled() )
142 {
143 components |= Qgis::TextComponent::Shadow;
144 }
145
146 if ( format.buffer().enabled() )
147 {
148 components |= Qgis::TextComponent::Buffer;
149 }
150
151 drawParts( point, rotation, alignment, document, metrics, context, format, components, Qgis::TextLayoutMode::Point );
152}
153
154void QgsTextRenderer::drawTextOnLine( const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &_format, double offsetAlongLine, double offsetFromLine )
155{
156 QgsTextFormat lFormat = _format;
157 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
158 lFormat.updateDataDefinedProperties( context );
159 lFormat = updateShadowPosition( lFormat );
160
161 // DO NOT USE _format in the following code, always use lFormat!!
162
163 // todo handle newlines??
164 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( {text}, lFormat );
165
166 drawDocumentOnLine( line, lFormat, document, context, offsetAlongLine, offsetFromLine );
167}
168
169void QgsTextRenderer::drawDocumentOnLine( const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine, double offsetFromLine )
170{
171 QPolygonF labelBaselineCurve = line;
172 if ( !qgsDoubleNear( offsetFromLine, 0 ) )
173 {
174 std::unique_ptr < QgsLineString > ring( QgsLineString::fromQPolygonF( line ) );
175 QgsGeos geos( ring.get() );
176 std::unique_ptr < QgsLineString > offsetCurve( dynamic_cast< QgsLineString * >( geos.offsetCurve( offsetFromLine, 4, Qgis::JoinStyle::Round, 2 ) ) );
177 if ( !offsetCurve )
178 return;
179
180#if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<11
181 if ( offsetFromLine < 0 )
182 {
183 // geos < 3.11 reverses the direction of offset curves with negative distances -- we don't want that!
184 std::unique_ptr < QgsLineString > reversed( offsetCurve->reversed() );
185 if ( !reversed )
186 return;
187
188 offsetCurve = std::move( reversed );
189 }
190#endif
191
192 labelBaselineCurve = offsetCurve->asQPolygonF();
193 }
194
195 const double fontScale = calculateScaleFactorForFormat( context, format );
196
197 const QFont baseFont = format.scaledFont( context, fontScale );
198 const double letterSpacing = baseFont.letterSpacing() / fontScale;
199 const double wordSpacing = baseFont.wordSpacing() / fontScale;
200
201 QStringList graphemes;
202 QVector< QgsTextCharacterFormat > graphemeFormats;
203 QVector< QgsTextDocumentMetrics > graphemeMetrics;
204
205 for ( const QgsTextBlock &block : std::as_const( document ) )
206 {
207 for ( const QgsTextFragment &fragment : block )
208 {
209 const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
210 for ( const QString &grapheme : fragmentGraphemes )
211 {
212 graphemes.append( grapheme );
213 graphemeFormats.append( fragment.characterFormat() );
214
215 QgsTextDocument document;
216 document.append( QgsTextBlock( QgsTextFragment( grapheme, fragment.characterFormat() ) ) );
217
218 graphemeMetrics.append( QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale ) );
219 }
220 }
221 }
222
223 QVector< double > characterWidths( graphemes.count() );
224 QVector< double > characterHeights( graphemes.count() );
225 QVector< double > characterDescents( graphemes.count() );
226 QFont previousNonSuperSubScriptFont;
227
228 for ( int i = 0; i < graphemes.count(); i++ )
229 {
230 // reconstruct how Qt creates word spacing, then adjust per individual stored character
231 // this will allow the text renderer to create each candidate width = character width + correct spacing
232
233 double graphemeFirstCharHorizontalAdvanceWithLetterSpacing = 0;
234 double graphemeFirstCharHorizontalAdvance = 0;
235 double graphemeHorizontalAdvance = 0;
236 double characterDescent = 0;
237 double characterHeight = 0;
238 const QgsTextCharacterFormat *graphemeFormat = &graphemeFormats[i];
239
240 QFont graphemeFont = baseFont;
241 graphemeFormat->updateFontForFormat( graphemeFont, context, fontScale );
242
243 if ( i == 0 )
244 previousNonSuperSubScriptFont = graphemeFont;
245
246 if ( graphemeFormat->hasVerticalAlignmentSet() )
247 {
248 switch ( graphemeFormat->verticalAlignment() )
249 {
251 previousNonSuperSubScriptFont = graphemeFont;
252 break;
253
256 {
257 if ( graphemeFormat->fontPointSize() < 0 )
258 {
259 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
260 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
261 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
262 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
263 // which we should respect
264 graphemeFont.setPixelSize( static_cast< int >( std::round( graphemeFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
265 }
266 break;
267 }
268 }
269 }
270 else
271 {
272 previousNonSuperSubScriptFont = graphemeFont;
273 }
274
275 const QFontMetricsF graphemeFontMetrics( graphemeFont );
276 graphemeFirstCharHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ) / fontScale;
277 graphemeFirstCharHorizontalAdvanceWithLetterSpacing = graphemeFontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) / fontScale + letterSpacing;
278 graphemeHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i] ) ) / fontScale;
279 characterDescent = graphemeFontMetrics.descent() / fontScale;
280 characterHeight = graphemeFontMetrics.height() / fontScale;
281
282 qreal wordSpaceFix = qreal( 0.0 );
283 if ( graphemes[i] == QLatin1String( " " ) )
284 {
285 // word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
286 int nxt = i + 1;
287 wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != QLatin1String( " " ) ) ? wordSpacing : qreal( 0.0 );
288 }
289
290 // this workaround only works for clusters with a single character. Not sure how it should be handled
291 // with multi-character clusters.
292 if ( graphemes[i].length() == 1 &&
293 !qgsDoubleNear( graphemeFirstCharHorizontalAdvance, graphemeFirstCharHorizontalAdvanceWithLetterSpacing ) )
294 {
295 // word spacing applied when it shouldn't be
296 wordSpaceFix -= wordSpacing;
297 }
298
299 const double charWidth = graphemeHorizontalAdvance + wordSpaceFix;
300 characterWidths[i] = charWidth;
301 characterHeights[i] = characterHeight;
302 characterDescents[i] = characterDescent;
303 }
304
305 QgsPrecalculatedTextMetrics metrics( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) );
306 metrics.setGraphemeFormats( graphemeFormats );
307
308 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement = QgsTextRendererUtils::generateCurvedTextPlacement(
309 metrics, labelBaselineCurve, offsetAlongLine,
311 -1, -1,
314 );
315
316 if ( placement->graphemePlacement.empty() )
317 return;
318
319 // We may have deliberately skipped over some graphemes during curved text placement (such as zero-width graphemes).
320 // So we need to use a hash of the original grapheme index to place generated components in, as there may accordingly
321 // be graphemes which don't result in components, and we can't just blindly assume the component array position
322 // will match the original grapheme index
323 QHash< int, QgsTextRenderer::Component > components;
324 components.reserve( placement->graphemePlacement.size() );
325 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
326 {
327 QgsTextRenderer::Component component;
328 component.origin = QPointF( grapheme.x, grapheme.y );
329 component.rotation = -grapheme.angle;
330
331 QgsTextDocumentMetrics &metrics = graphemeMetrics[ grapheme.graphemeIndex ];
332 const double verticalOffset = metrics.fragmentVerticalOffset( 0, 0, Qgis::TextLayoutMode::Point );
333 if ( !qgsDoubleNear( verticalOffset, 0 ) )
334 {
335 component.origin.rx() += verticalOffset * std::cos( grapheme.angle + M_PI_2 );
336 component.origin.ry() += verticalOffset * std::sin( grapheme.angle + M_PI_2 );
337 }
338
339 components.insert( grapheme.graphemeIndex, component );
340 }
341
342 if ( format.background().enabled() )
343 {
344 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
345 {
346 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
347 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
348 drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Point );
349 }
350 }
351
352 if ( format.buffer().enabled() )
353 {
354 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
355 {
356 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
357 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
358
359 drawTextInternal( Qgis::TextComponent::Buffer,
360 context,
361 format,
362 component,
363 metrics.document(),
364 metrics,
368 }
369 }
370
371 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
372 {
373 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
374 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
375
376 drawTextInternal( Qgis::TextComponent::Text,
377 context,
378 format,
379 component,
380 metrics.document(),
381 metrics,
385 }
386}
387
388QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format )
389{
391 return format;
392
393 QgsTextFormat tmpFormat = format;
394 if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds
395 {
397 }
398 else if ( tmpFormat.buffer().enabled() )
399 {
401 }
402 else
403 {
405 }
406 return tmpFormat;
407}
408
409void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment,
410 const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
411{
412 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
413 const double fontScale = calculateScaleFactorForFormat( context, format );
414 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
415
416 drawParts( rect, rotation, alignment, Qgis::TextVerticalAlignment::Top, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Rectangle );
417}
418
419void 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 )
420{
421 if ( !context.painter() )
422 {
423 return;
424 }
425
426 Component component;
427 component.dpiRatio = 1.0;
428 component.origin = rect.topLeft();
429 component.rotation = rotation;
430 component.size = rect.size();
431 component.hAlign = alignment;
432
433 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
434 {
435 if ( !qgsDoubleNear( rotation, 0.0 ) )
436 {
437 // get rotated label's center point
438
439 double xc = rect.width() / 2.0;
440 double yc = rect.height() / 2.0;
441
442 double angle = -rotation;
443 double xd = xc * std::cos( angle ) - yc * std::sin( angle );
444 double yd = xc * std::sin( angle ) + yc * std::cos( angle );
445
446 component.center = QPointF( component.origin.x() + xd, component.origin.y() + yd );
447 }
448 else
449 {
450 component.center = rect.center();
451 }
452
453 switch ( vAlignment )
454 {
456 break;
458 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
459 break;
461 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
462 break;
463 }
464
465 QgsTextRenderer::drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Rectangle );
466 }
467
468 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
469 {
470 return;
471 }
472
474 {
475 drawTextInternal( parts, context, format, component,
476 document, metrics,
477 alignment, vAlignment, mode );
478 }
479}
480
481void QgsTextRenderer::drawPart( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
482{
483 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
484 const double fontScale = calculateScaleFactorForFormat( context, format );
485 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
486
487 drawParts( origin, rotation, alignment, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Point );
488}
489
490void 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 )
491{
492 if ( !context.painter() )
493 {
494 return;
495 }
496
497 Component component;
498 component.dpiRatio = 1.0;
499 component.origin = origin;
500 component.rotation = rotation;
501 component.hAlign = alignment;
502
503 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
504 {
505 QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
506 }
507
508 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
509 {
510 return;
511 }
512
514 {
515 drawTextInternal( parts, context, format, component,
516 document,
517 metrics,
519 mode );
520 }
521}
522
523QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
524{
525 return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
526}
527
528double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format,
529 const QgsTextDocumentMetrics &metrics,
531{
532 QPainter *p = context.painter();
533
534 Qgis::TextOrientation orientation = format.orientation();
536 {
537 if ( component.rotation >= -315 && component.rotation < -90 )
538 {
540 }
541 else if ( component.rotation >= -90 && component.rotation < -45 )
542 {
544 }
545 else
546 {
548 }
549 }
550
551 QgsTextBufferSettings buffer = format.buffer();
552
553 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage
554 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
555 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
556
557 const double scaleFactor = calculateScaleFactorForFormat( context, format );
558
559 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
560 if ( mode == Qgis::TextLayoutMode::Labeling )
561 {
562 // label size has already been calculated using any symbology reference scale factor -- we need
563 // to temporarily remove the reference scale here or we'll be applying the scaling twice
564 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
565 }
566
567 if ( metrics.isNullFontSize() )
568 return 0;
569
570 referenceScaleOverride.reset();
571
572 QPainterPath path;
573 path.setFillRule( Qt::WindingFill );
574 double advance = 0;
575 double height = component.size.height();
576 switch ( orientation )
577 {
579 {
580 // NOT SUPPORTED BY THIS METHOD ANYMORE -- buffer drawing is handled in drawTextInternalHorizontal since QGIS 3.42
581 break;
582 }
583
586 {
587 double partYOffset = component.offset.y() * scaleFactor;
588
589 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
590 double partLastDescent = 0;
591
592 int fragmentIndex = 0;
593 for ( const QgsTextFragment &fragment : component.block )
594 {
595 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
596 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
597
598 const QFontMetricsF fragmentMetrics( fragmentFont );
599
600 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
601
602 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
603 for ( const QString &part : parts )
604 {
605 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
606 partYOffset += fragmentMetrics.ascent() / scaleFactor;
607 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
608 partYOffset += letterSpacing;
609 }
610 partLastDescent = fragmentMetrics.descent() / scaleFactor;
611
612 fragmentIndex++;
613 }
614 height = partYOffset + partLastDescent;
615 advance = partYOffset - component.offset.y() * scaleFactor;
616 break;
617 }
618 }
619
620 QColor bufferColor = buffer.color();
621 bufferColor.setAlphaF( buffer.opacity() );
622 QPen pen( bufferColor );
623 pen.setWidthF( penSize * scaleFactor );
624 pen.setJoinStyle( buffer.joinStyle() );
625 QColor tmpColor( bufferColor );
626 // honor pref for whether to fill buffer interior
627 if ( !buffer.fillBufferInterior() )
628 {
629 tmpColor.setAlpha( 0 );
630 }
631
632 // store buffer's drawing in QPicture for drop shadow call
633 QPicture buffPict;
634 QPainter buffp;
635 buffp.begin( &buffPict );
636 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
637 {
638 context.setPainter( &buffp );
639 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
640
641 tmpEffect->begin( context );
642 context.painter()->setPen( pen );
643 context.painter()->setBrush( tmpColor );
644 if ( scaleFactor != 1.0 )
645 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
646 context.painter()->drawPath( path );
647 if ( scaleFactor != 1.0 )
648 context.painter()->scale( scaleFactor, scaleFactor );
649 tmpEffect->end( context );
650
651 context.setPainter( p );
652 }
653 else
654 {
655 if ( scaleFactor != 1.0 )
656 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
657 buffp.setPen( pen );
658 buffp.setBrush( tmpColor );
659 buffp.drawPath( path );
660 }
661 buffp.end();
662
664 {
665 QgsTextRenderer::Component bufferComponent = component;
666 bufferComponent.origin = QPointF( 0.0, 0.0 );
667 bufferComponent.picture = buffPict;
668 bufferComponent.pictureBuffer = penSize / 2.0;
669 bufferComponent.size.setHeight( height );
670
672 {
673 bufferComponent.offset.setY( - bufferComponent.size.height() );
674 }
675 drawShadow( context, bufferComponent, format );
676 }
677
678 QgsScopedQPainterState painterState( p );
679 context.setPainterFlagsUsingContext( p );
680
681 if ( context.useAdvancedEffects() )
682 {
683 p->setCompositionMode( buffer.blendMode() );
684 }
685
686 // scale for any print output or image saving @ specific dpi
687 p->scale( component.dpiRatio, component.dpiRatio );
689 p->drawPicture( 0, 0, buffPict );
690
691 return advance / scaleFactor;
692}
693
694void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics,
696{
697 QgsTextMaskSettings mask = format.mask();
698
699 // the mask is drawn to a side painter
700 // or to the main painter for preview
701 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
702 if ( ! p )
703 return;
704
705 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage
706 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
707 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
708
709 // buffer: draw the text with a big pen
710 QPainterPath path;
711 path.setFillRule( Qt::WindingFill );
712
713 const double scaleFactor = calculateScaleFactorForFormat( context, format );
714
715 // TODO: vertical text mode was ignored when masking feature was added.
716 // Hopefully Oslandia come back and fix this? Hint hint...
717
718 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
719 if ( mode == Qgis::TextLayoutMode::Labeling )
720 {
721 // label size has already been calculated using any symbology reference scale factor -- we need
722 // to temporarily remove the reference scale here or we'll be applying the scaling twice
723 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
724 }
725
726 if ( metrics.isNullFontSize() )
727 return;
728
729 referenceScaleOverride.reset();
730
731 double xOffset = 0;
732 int fragmentIndex = 0;
733 for ( const QgsTextFragment &fragment : component.block )
734 {
735 if ( !fragment.isWhitespace() && !fragment.isImage() )
736 {
737 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
738
739 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
740 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
741 }
742
743 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
744 fragmentIndex++;
745 }
746
747 QColor bufferColor( Qt::gray );
748 bufferColor.setAlphaF( mask.opacity() );
749
750 QPen pen;
751 QBrush brush;
752 brush.setColor( bufferColor );
753 pen.setColor( bufferColor );
754 pen.setWidthF( penSize * scaleFactor );
755 pen.setJoinStyle( mask.joinStyle() );
756
757 QgsScopedQPainterState painterState( p );
758 context.setPainterFlagsUsingContext( p );
759
760 // scale for any print output or image saving @ specific dpi
761 p->scale( component.dpiRatio, component.dpiRatio );
762 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
763 {
764 QgsPainterSwapper swapper( context, p );
765 {
766 QgsEffectPainter effectPainter( context, mask.paintEffect() );
767 if ( scaleFactor != 1.0 )
768 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
769 context.painter()->setPen( pen );
770 context.painter()->setBrush( brush );
771 context.painter()->drawPath( path );
772 if ( scaleFactor != 1.0 )
773 context.painter()->scale( scaleFactor, scaleFactor );
774 }
775 }
776 else
777 {
778 if ( scaleFactor != 1.0 )
779 p->scale( 1 / scaleFactor, 1 / scaleFactor );
780 p->setPen( pen );
781 p->setBrush( brush );
782 p->drawPath( path );
783 if ( scaleFactor != 1.0 )
784 p->scale( scaleFactor, scaleFactor );
785
786 }
787}
788
789double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
790{
791 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( textLines, format );
792 if ( doc.size() == 0 )
793 return 0;
794
795 return textWidth( context, format, doc );
796}
797
798double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
799{
800 //calculate max width of text lines
801 const double scaleFactor = calculateScaleFactorForFormat( context, format );
802
803 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
804
805 // width doesn't change depending on layout mode, we can use anything here
806 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
807}
808
809double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth )
810{
811 QStringList lines;
812 for ( const QString &line : textLines )
813 {
814 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
815 {
816 lines.append( wrappedText( context, line, maxLineWidth, format ) );
817 }
818 else
819 {
820 lines.append( line );
821 }
822 }
823
824 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( lines, format );
825 return textHeight( context, format, doc, mode );
826}
827
828double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
829{
830 const double scaleFactor = calculateScaleFactorForFormat( context, format );
831
832 bool isNullSize = false;
833 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
834 if ( isNullSize )
835 return 0;
836
837 const QFontMetrics fm( baseFont );
838 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
839
840 if ( !includeEffects )
841 return height;
842
843 double maxExtension = 0;
844 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
845 if ( format.buffer().enabled() )
846 {
847 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
848 ? fontSize * format.buffer().size() / 100
849 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
850 }
851 if ( format.shadow().enabled() )
852 {
853 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
854 ? fontSize * format.shadow().offsetDistance() / 100
855 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
856 )
858 ? fontSize * format.shadow().blurRadius() / 100
859 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
860 );
861 }
862 if ( format.background().enabled() )
863 {
864 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
866 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
867 {
868 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
869 }
870 }
871
872 return height + maxExtension;
873}
874
875bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
876{
877 if ( qgsDoubleNear( width, 0.0 ) )
878 return false;
879
880 const QStringList multiLineSplit = text.split( '\n' );
881 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
882 return currentTextWidth > width;
883}
884
885QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
886{
887 const QStringList lines = text.split( '\n' );
888 QStringList outLines;
889 for ( const QString &line : lines )
890 {
891 if ( textRequiresWrapping( context, line, width, format ) )
892 {
893 //first step is to identify words which must be on their own line (too long to fit)
894 const QStringList words = line.split( ' ' );
895 QStringList linesToProcess;
896 QString wordsInCurrentLine;
897 for ( const QString &word : words )
898 {
899 if ( textRequiresWrapping( context, word, width, format ) )
900 {
901 //too long to fit
902 if ( !wordsInCurrentLine.isEmpty() )
903 linesToProcess << wordsInCurrentLine;
904 wordsInCurrentLine.clear();
905 linesToProcess << word;
906 }
907 else
908 {
909 if ( !wordsInCurrentLine.isEmpty() )
910 wordsInCurrentLine.append( ' ' );
911 wordsInCurrentLine.append( word );
912 }
913 }
914 if ( !wordsInCurrentLine.isEmpty() )
915 linesToProcess << wordsInCurrentLine;
916
917 for ( const QString &line : std::as_const( linesToProcess ) )
918 {
919 QString remainingText = line;
920 int lastPos = remainingText.lastIndexOf( ' ' );
921 while ( lastPos > -1 )
922 {
923 //check if remaining text is short enough to go in one line
924 if ( !textRequiresWrapping( context, remainingText, width, format ) )
925 {
926 break;
927 }
928
929 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
930 {
931 outLines << remainingText.left( lastPos );
932 remainingText = remainingText.mid( lastPos + 1 );
933 lastPos = 0;
934 }
935 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
936 }
937 outLines << remainingText;
938 }
939 }
940 else
941 {
942 outLines << line;
943 }
944 }
945
946 return outLines;
947}
948
949double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
950{
951 QgsTextDocument document = doc;
952 document.applyCapitalization( format.capitalization() );
953
954 //calculate max height of text lines
955 const double scaleFactor = calculateScaleFactorForFormat( context, format );
956
957 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
958 if ( metrics.isNullFontSize() )
959 return 0;
960
961 return metrics.documentSize( mode, format.orientation() ).height();
962}
963
964void QgsTextRenderer::drawBackground( QgsRenderContext &context, const QgsTextRenderer::Component &c, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
965{
966 Component component = c;
967 QgsTextBackgroundSettings background = format.background();
968
969 QPainter *prevP = context.painter();
970 QPainter *p = context.painter();
971 std::unique_ptr< QgsPaintEffect > tmpEffect;
972 if ( background.paintEffect() && background.paintEffect()->enabled() )
973 {
974 tmpEffect.reset( background.paintEffect()->clone() );
975 tmpEffect->begin( context );
976 p = context.painter();
977 }
978
979 //QgsDebugMsgLevel( QStringLiteral( "Background label rotation: %1" ).arg( component.rotation() ), 4 );
980
981 // shared calculations between shapes and SVG
982
983 // configure angles, set component rotation and rotationOffset
984 const double originAdjustRotationRadians = -component.rotation;
986 {
987 component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
988 component.rotationOffset =
989 background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
990 }
991 else // RotationFixed
992 {
993 component.rotation = 0.0; // don't use label's rotation
994 component.rotationOffset = background.rotation();
995 }
996
997 const double scaleFactor = calculateScaleFactorForFormat( context, format );
998
999 if ( mode != Qgis::TextLayoutMode::Labeling )
1000 {
1001 // need to calculate size of text
1002 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1003 double width = documentSize.width();
1004 double height = documentSize.height();
1005
1006 switch ( mode )
1007 {
1011 switch ( component.hAlign )
1012 {
1015 component.center = QPointF( component.origin.x() + width / 2.0,
1016 component.origin.y() + height / 2.0 );
1017 break;
1018
1020 component.center = QPointF( component.origin.x() + component.size.width() / 2.0,
1021 component.origin.y() + height / 2.0 );
1022 break;
1023
1025 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0,
1026 component.origin.y() + height / 2.0 );
1027 break;
1028 }
1029 break;
1030
1032 {
1033 bool isNullSize = false;
1034 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1035 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1036 switch ( component.hAlign )
1037 {
1040 component.center = QPointF( component.origin.x() + width / 2.0,
1041 component.origin.y() - height / 2.0 + originAdjust );
1042 break;
1043
1045 component.center = QPointF( component.origin.x(),
1046 component.origin.y() - height / 2.0 + originAdjust );
1047 break;
1048
1050 component.center = QPointF( component.origin.x() - width / 2.0,
1051 component.origin.y() - height / 2.0 + originAdjust );
1052 break;
1053 }
1054
1055 // apply rotation to center point
1056 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1057 {
1058 const double dx = component.center.x() - component.origin.x();
1059 const double dy = component.center.y() - component.origin.y();
1060 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1061 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1062 }
1063 break;
1064 }
1065
1067 break;
1068 }
1069
1071 component.size = QSizeF( width, height );
1072 }
1073
1074 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1075
1076 switch ( background.type() )
1077 {
1080 {
1081 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1082
1083 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1084 return;
1085
1086 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1087 return;
1088
1089 double sizeOut = 0.0;
1090 {
1091 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1092
1093 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1094 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1095 {
1096 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1097 }
1098 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1099 {
1100 sizeOut = std::max( component.size.width(), component.size.height() );
1101 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1102
1103 // add buffer
1104 sizeOut += bufferSize * 2;
1105 }
1106 }
1107
1108 // don't bother rendering symbols smaller than 1x1 pixels in size
1109 // TODO: add option to not show any svgs under/over a certain size
1110 if ( sizeOut < 1.0 )
1111 return;
1112
1113 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1114 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1115 {
1116 QVariantMap map; // for SVG symbology marker
1117 map[QStringLiteral( "name" )] = background.svgFile().trimmed();
1118 map[QStringLiteral( "size" )] = QString::number( sizeOut );
1119 map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1120 map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter
1121
1122 // offset is handled by this local painter
1123 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1124 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1125 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1126 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1127
1128 map[QStringLiteral( "fill" )] = background.fillColor().name();
1129 map[QStringLiteral( "outline" )] = background.strokeColor().name();
1130 map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() );
1131 map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1132
1134 {
1135 QgsTextShadowSettings shadow = format.shadow();
1136 // configure SVG shadow specs
1137 QVariantMap shdwmap( map );
1138 shdwmap[QStringLiteral( "fill" )] = shadow.color().name();
1139 shdwmap[QStringLiteral( "outline" )] = shadow.color().name();
1140 shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut );
1141
1142 // store SVG's drawing in QPicture for drop shadow call
1143 QPicture svgPict;
1144 QPainter svgp;
1145 svgp.begin( &svgPict );
1146
1147 // draw shadow symbol
1148
1149 // clone current render context map unit/mm conversion factors, but not
1150 // other map canvas parameters, then substitute this painter for use in symbology painting
1151 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1152 // but will be created relative to the SVG's computed size, not the current map canvas
1153 QgsRenderContext shdwContext;
1154 shdwContext.setMapToPixel( context.mapToPixel() );
1155 shdwContext.setScaleFactor( context.scaleFactor() );
1156 shdwContext.setPainter( &svgp );
1157
1158 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1159 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1160 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1161
1162 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1163 svgp.end();
1164
1165 component.picture = svgPict;
1166 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1167 component.pictureBuffer = 0.0;
1168
1169 component.size = QSizeF( sizeOut, sizeOut );
1170 component.offset = QPointF( 0.0, 0.0 );
1171
1172 // rotate about origin center of SVG
1173 QgsScopedQPainterState painterState( p );
1174 context.setPainterFlagsUsingContext( p );
1175
1176 p->translate( component.center.x(), component.center.y() );
1177 p->rotate( component.rotation );
1178 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1179 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1180 p->translate( QPointF( xoff, yoff ) );
1181 p->rotate( component.rotationOffset );
1182 p->translate( -sizeOut / 2, sizeOut / 2 );
1183
1184 drawShadow( context, component, format );
1185 }
1186 renderedSymbol.reset( );
1187
1189 renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) );
1190 }
1191 else
1192 {
1193 renderedSymbol.reset( background.markerSymbol()->clone() );
1194 renderedSymbol->setSize( sizeOut );
1195 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1196 }
1197
1198 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1199
1200 // draw the actual symbol
1201 QgsScopedQPainterState painterState( p );
1202 context.setPainterFlagsUsingContext( p );
1203
1204 if ( context.useAdvancedEffects() )
1205 {
1206 p->setCompositionMode( background.blendMode() );
1207 }
1208 p->translate( component.center.x(), component.center.y() );
1209 p->rotate( component.rotation );
1210 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1211 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1212 p->translate( QPointF( xoff, yoff ) );
1213 p->rotate( component.rotationOffset );
1214
1215 const QgsFeature f = context.expressionContext().feature();
1216 renderedSymbol->startRender( context, context.expressionContext().fields() );
1217 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1218 renderedSymbol->stopRender( context );
1219 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1220
1221 break;
1222 }
1223
1228 {
1229 double w = component.size.width();
1230 double h = component.size.height();
1231
1232 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1233 {
1234 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1235 background.sizeMapUnitScale() );
1236 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1237 background.sizeMapUnitScale() );
1238 }
1239 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1240 {
1241 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1242 {
1243 if ( w > h )
1244 h = w;
1245 else if ( h > w )
1246 w = h;
1247 }
1248 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1249 {
1250 // start with label bound by circle
1251 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1252 w = h;
1253 }
1254 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1255 {
1256 // start with label bound by ellipse
1257 h = h * M_SQRT1_2 * 2;
1258 w = w * M_SQRT1_2 * 2;
1259 }
1260
1261 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1262 background.sizeMapUnitScale() );
1263 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1264 background.sizeMapUnitScale() );
1265
1266 w += bufferWidth * 2;
1267 h += bufferHeight * 2;
1268 }
1269
1270 // offsets match those of symbology: -x = left, -y = up
1271 QRectF rect( -w / 2.0, - h / 2.0, w, h );
1272
1273 if ( rect.isNull() )
1274 return;
1275
1276 QgsScopedQPainterState painterState( p );
1277 context.setPainterFlagsUsingContext( p );
1278
1279 p->translate( QPointF( component.center.x(), component.center.y() ) );
1280 p->rotate( component.rotation );
1281 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1282 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1283 p->translate( QPointF( xoff, yoff ) );
1284 p->rotate( component.rotationOffset );
1285
1286 QPainterPath path;
1287
1288 // Paths with curves must be enlarged before conversion to QPolygonF, or
1289 // the curves are approximated too much and appear jaggy
1290 QTransform t = QTransform::fromScale( 10, 10 );
1291 // inverse transform used to scale created polygons back to expected size
1292 QTransform ti = t.inverted();
1293
1295 || background.type() == QgsTextBackgroundSettings::ShapeSquare )
1296 {
1297 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1298 {
1299 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1300 }
1301 else
1302 {
1303 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1304 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1305 path.addRoundedRect( rect, xRadius, yRadius );
1306 }
1307 }
1308 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse
1309 || background.type() == QgsTextBackgroundSettings::ShapeCircle )
1310 {
1311 path.addEllipse( rect );
1312 }
1313 QPolygonF tempPolygon = path.toFillPolygon( t );
1314 QPolygonF polygon = ti.map( tempPolygon );
1315 QPicture shapePict;
1316 QPainter *oldp = context.painter();
1317 QPainter shapep;
1318
1319 shapep.begin( &shapePict );
1320 context.setPainter( &shapep );
1321
1322 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1323 renderedSymbol.reset( background.fillSymbol()->clone() );
1324 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1325
1326 const QgsFeature f = context.expressionContext().feature();
1327 renderedSymbol->startRender( context, context.expressionContext().fields() );
1328 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1329 renderedSymbol->stopRender( context );
1330
1331 shapep.end();
1332 context.setPainter( oldp );
1333
1335 {
1336 component.picture = shapePict;
1337 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1338
1339 component.size = rect.size();
1340 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1341 drawShadow( context, component, format );
1342 }
1343
1344 if ( context.useAdvancedEffects() )
1345 {
1346 p->setCompositionMode( background.blendMode() );
1347 }
1348
1349 // scale for any print output or image saving @ specific dpi
1350 p->scale( component.dpiRatio, component.dpiRatio );
1352 p->drawPicture( 0, 0, shapePict );
1353 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1354 break;
1355 }
1356 }
1357
1358 if ( tmpEffect )
1359 {
1360 tmpEffect->end( context );
1361 context.setPainter( prevP );
1362 }
1363}
1364
1365void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1366{
1367 QgsTextShadowSettings shadow = format.shadow();
1368
1369 QPainter *p = context.painter();
1370 const double componentWidth = component.size.width();
1371 const double componentHeight = component.size.height();
1372 const double xOffset = component.offset.x();
1373 const double yOffset = component.offset.y();
1374 double pictbuffer = component.pictureBuffer;
1375
1376 // generate pixmap representation of label component drawing
1377 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1378
1379 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1380 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage
1381 ? fontSize * shadow.blurRadius() / 100
1382 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1383 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1384 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1385
1386 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1387 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1388 double blurBufferClippingScale = 3.75;
1389 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1390
1391 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1392 componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1393 QImage::Format_ARGB32_Premultiplied );
1394
1395 // TODO: add labeling gui option to not show any shadows under/over a certain size
1396 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1397 int minBlurImgSize = 1;
1398 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1399 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1400 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1401 int maxBlurImgSize = 40000;
1402 if ( blurImg.isNull()
1403 || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize )
1404 || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1405 return;
1406
1407 blurImg.fill( QColor( Qt::transparent ).rgba() );
1408 QPainter pictp;
1409 if ( !pictp.begin( &blurImg ) )
1410 return;
1411 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1412 QPointF imgOffset( blurbuffer + pictbuffer + xOffset,
1413 blurbuffer + pictbuffer + componentHeight + yOffset );
1414
1415 pictp.drawPicture( imgOffset,
1416 component.picture );
1417
1418 // overlay shadow color
1419 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1420 pictp.fillRect( blurImg.rect(), shadow.color() );
1421 pictp.end();
1422
1423 // blur the QImage in-place
1424 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1425 {
1426 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1427 }
1428
1429#if 0
1430 // debug rect for QImage shadow registration and clipping visualization
1431 QPainter picti;
1432 picti.begin( &blurImg );
1433 picti.setBrush( Qt::Dense7Pattern );
1434 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1435 imgPen.setWidth( 1 );
1436 picti.setPen( imgPen );
1437 picti.setOpacity( 0.1 );
1438 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1439 picti.end();
1440#endif
1441
1442 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage
1443 ? fontSize * shadow.offsetDistance() / 100
1444 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1445 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1446 if ( shadow.offsetGlobal() )
1447 {
1448 // TODO: check for differences in rotation origin and cw/ccw direction,
1449 // when this shadow function is used for something other than labels
1450
1451 // it's 0-->cw-->360 for labels
1452 //QgsDebugMsgLevel( QStringLiteral( "Shadow aggregated label rotation (degrees): %1" ).arg( component.rotation() + component.rotationOffset() ), 4 );
1453 angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1454 }
1455
1456 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ),
1457 -offsetDist * std::sin( angleRad + M_PI_2 ) );
1458
1459 p->save();
1460 context.setPainterFlagsUsingContext( p );
1461 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1462 // corresponding flag in the render context instead...
1463 p->setRenderHint( QPainter::SmoothPixmapTransform );
1464 if ( context.useAdvancedEffects() )
1465 {
1466 p->setCompositionMode( shadow.blendMode() );
1467 }
1468 p->setOpacity( shadow.opacity() );
1469
1470 double scale = shadow.scale() / 100.0;
1471 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1472 p->scale( scale, scale );
1473 if ( component.useOrigin )
1474 {
1475 p->translate( component.origin.x(), component.origin.y() );
1476 }
1477 p->translate( transPt );
1478 p->translate( -imgOffset.x(),
1479 -imgOffset.y() );
1480 p->drawImage( 0, 0, blurImg );
1481 p->restore();
1482
1483 // debug rects
1484#if 0
1485 // draw debug rect for QImage painting registration
1486 p->save();
1487 p->setBrush( Qt::NoBrush );
1488 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1489 imgPen.setWidth( 2 );
1490 imgPen.setStyle( Qt::DashLine );
1491 p->setPen( imgPen );
1492 p->scale( scale, scale );
1493 if ( component.useOrigin() )
1494 {
1495 p->translate( component.origin().x(), component.origin().y() );
1496 }
1497 p->translate( transPt );
1498 p->translate( -imgOffset.x(),
1499 -imgOffset.y() );
1500 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1501 p->restore();
1502
1503 // draw debug rect for passed in component dimensions
1504 p->save();
1505 p->setBrush( Qt::NoBrush );
1506 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1507 componentRectPen.setWidth( 1 );
1508 if ( component.useOrigin() )
1509 {
1510 p->translate( component.origin().x(), component.origin().y() );
1511 }
1512 p->setPen( componentRectPen );
1513 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1514 p->restore();
1515#endif
1516}
1517
1518
1519void QgsTextRenderer::drawTextInternal( Qgis::TextComponents components,
1520 QgsRenderContext &context,
1521 const QgsTextFormat &format,
1522 const Component &component,
1523 const QgsTextDocument &document,
1524 const QgsTextDocumentMetrics &metrics,
1526{
1527 if ( !context.painter() )
1528 {
1529 return;
1530 }
1531
1532 const double fontScale = calculateScaleFactorForFormat( context, format );
1533
1534 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1535 if ( mode == Qgis::TextLayoutMode::Labeling )
1536 {
1537 // label size has already been calculated using any symbology reference scale factor -- we need
1538 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1539 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1540 }
1541
1542 if ( metrics.isNullFontSize() )
1543 return;
1544
1545 referenceScaleOverride.reset();
1546
1547 double rotation = 0;
1548 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1549 switch ( orientation )
1550 {
1552 {
1553 drawTextInternalHorizontal( context, format, components, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1554 break;
1555 }
1556
1559 {
1560 // TODO: vertical text renderer currently doesn't handle one-pass buffer + text drawing
1561 if ( components & Qgis::TextComponent::Buffer )
1562 drawTextInternalVertical( context, format, Qgis::TextComponent::Buffer, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1563 if ( components & Qgis::TextComponent::Text )
1564 drawTextInternalVertical( context, format, Qgis::TextComponent::Text, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1565 break;
1566 }
1567 }
1568}
1569
1570Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1571{
1572 rotation = -component.rotation * 180 / M_PI;
1573
1574 switch ( format.orientation() )
1575 {
1577 {
1578 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1579 if ( rotation >= -315 && rotation < -90 )
1580 {
1581 rotation -= 90;
1583 }
1584 else if ( rotation >= -90 && rotation < -45 )
1585 {
1586 rotation += 90;
1588 }
1589
1591 }
1592
1595 return format.orientation();
1596 }
1598}
1599
1600void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1601{
1602 const QString blockText = block.toPlainText();
1603 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1604 finder.toStart();
1605 int wordBoundaries = 0;
1606 while ( finder.toNextBoundary() != -1 )
1607 {
1608 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1609 wordBoundaries++;
1610 }
1611
1612 if ( wordBoundaries > 0 )
1613 {
1614 // word boundaries found => justify by padding word spacing
1615 extraWordSpace = spaceToDistribute / wordBoundaries;
1616 }
1617 else
1618 {
1619 // no word boundaries found => justify by letter spacing
1620 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1621 finder.toStart();
1622
1623 int graphemeBoundaries = 0;
1624 while ( finder.toNextBoundary() != -1 )
1625 {
1626 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1627 graphemeBoundaries++;
1628 }
1629
1630 if ( graphemeBoundaries > 0 )
1631 {
1632 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1633 }
1634 }
1635}
1636
1637void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1638{
1639 const double prevWordSpace = font.wordSpacing();
1640 font.setWordSpacing( prevWordSpace + extraWordSpace );
1641 const double prevLetterSpace = font.letterSpacing();
1642 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1643}
1644
1645
1646void QgsTextRenderer::renderBlockHorizontal( const QgsTextBlock &block, int blockIndex,
1647 const QgsTextDocumentMetrics &metrics, QgsRenderContext &context,
1648 const QgsTextFormat &format,
1649 QPainter *painter, bool forceRenderAsPaths,
1650 double fontScale, double extraWordSpace, double extraLetterSpace,
1651 Qgis::TextLayoutMode mode, DeferredRenderBlock *deferredRenderBlock )
1652{
1653 if ( !metrics.isNullFontSize() )
1654 {
1655 double xOffset = 0;
1656 int fragmentIndex = 0;
1657 for ( const QgsTextFragment &fragment : block )
1658 {
1659 // draw text, QPainterPath method
1660 if ( !fragment.isWhitespace() && !fragment.isImage() )
1661 {
1662 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1663
1664 if ( !qgsDoubleNear( extraWordSpace, 0 ) || !qgsDoubleNear( extraLetterSpace, 0 ) )
1665 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1666
1667 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1668
1669 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1670 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1671
1672 if ( deferredRenderBlock )
1673 {
1674 DeferredRenderFragment renderFragment;
1675 renderFragment.color = textColor;
1676 if ( forceRenderAsPaths )
1677 {
1678 renderFragment.path.setFillRule( Qt::WindingFill );
1679 renderFragment.path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1680 }
1681 renderFragment.font = fragmentFont;
1682 renderFragment.point = QPointF( xOffset, yOffset );
1683 renderFragment.text = fragment.text();
1684 deferredRenderBlock->fragments.append( renderFragment );
1685 }
1686 else if ( forceRenderAsPaths )
1687 {
1688 painter->setBrush( textColor );
1689 QPainterPath path;
1690 path.setFillRule( Qt::WindingFill );
1691 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1692 painter->drawPath( path );
1693 }
1694 else
1695 {
1696 painter->setPen( textColor );
1697 painter->setFont( fragmentFont );
1698 painter->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1699 }
1700 }
1701 else if ( fragment.isImage() )
1702 {
1703 bool fitsInCache = false;
1704 const double imageWidth = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1705 const double imageHeight = metrics.fragmentFixedHeight( blockIndex, fragmentIndex, mode ) * fontScale;
1706
1707 const QImage image = QgsApplication::imageCache()->pathAsImage( fragment.characterFormat().imagePath(),
1708 QSize( static_cast< int >( std::round( imageWidth ) ),
1709 static_cast< int >( std::round( imageHeight ) ) ),
1710 false,
1711 1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1712 const double imageBaseline = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1713 const double yOffset = imageBaseline - image.height();
1714 if ( !image.isNull() )
1715 painter->drawImage( QPointF( xOffset, yOffset ), image );
1716 }
1717
1718 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1719 fragmentIndex ++;
1720 }
1721 }
1722};
1723
1724bool QgsTextRenderer::usePathsToRender( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
1725{
1726 switch ( context.textRenderFormat() )
1727 {
1729 return true;
1731 return false;
1733 {
1734 // Prefer not to use paths -- but certain conditions will require us to use them
1735 if ( format.buffer().enabled() )
1736 {
1737 // text buffer requires use of paths
1738 // TODO: this was the original cause of use switching from text to paths by default,
1739 // but that was way back in the 2.0 days and maybe the Qt issues have now been fixed?
1740 return true;
1741 }
1742
1743 // underline/overline/strikethrough looks different between path/non-path renders.
1744 // TODO: validate which is correct. For now, maintain default appearance from before this code
1745 // was introduced
1746 if ( format.font().underline()
1747 || format.font().overline()
1748 || format.font().strikeOut()
1749 || std::any_of( document.begin(), document.end(), []( const QgsTextBlock & block )
1750 {
1751 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment & fragment )
1752 {
1753 return fragment.characterFormat().underline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1754 || fragment.characterFormat().overline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1755 || fragment.characterFormat().strikeOut() == QgsTextCharacterFormat::BooleanValue::SetTrue;
1756 } );
1757 } ) )
1758 return true;
1759
1760 return false;
1761 }
1762 }
1764}
1765
1766bool QgsTextRenderer::usePictureToRender( const QgsRenderContext &, const QgsTextFormat &, const QgsTextDocument &document )
1767{
1768 return std::any_of( document.begin(), document.end(), []( const QgsTextBlock & block )
1769 {
1770 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment & fragment )
1771 {
1772 return fragment.isImage();
1773 } );
1774 } );
1775}
1776
1777QVector< QgsTextRenderer::BlockMetrics > QgsTextRenderer::calculateBlockMetrics( const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode, double targetWidth, const Qgis::TextHorizontalAlignment hAlignment )
1778{
1779 QVector< BlockMetrics > blockMetrics;
1780 blockMetrics.reserve( document.size() );
1781
1782 int blockIndex = 0;
1783 for ( const QgsTextBlock &block : document )
1784 {
1785 Qgis::TextHorizontalAlignment blockAlignment = hAlignment;
1786 if ( block.blockFormat().hasHorizontalAlignmentSet() )
1787 blockAlignment = block.blockFormat().horizontalAlignment();
1788 const bool adjustForAlignment = blockAlignment != Qgis::TextHorizontalAlignment::Left &&
1790 || document.size() > 1 );
1791
1792 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 )
1793 || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1794
1795 BlockMetrics thisBlockMetrics;
1796 // figure x offset for horizontal alignment of multiple lines
1797 thisBlockMetrics.width = metrics.blockWidth( blockIndex );
1798
1799 if ( adjustForAlignment )
1800 {
1801 double blockWidthDiff = 0;
1802 switch ( blockAlignment )
1803 {
1805 blockWidthDiff = ( targetWidth - thisBlockMetrics.width - metrics.blockLeftMargin( blockIndex ) - metrics.blockRightMargin( blockIndex ) ) * 0.5 + metrics.blockLeftMargin( blockIndex );
1806 break;
1807
1809 blockWidthDiff = targetWidth - thisBlockMetrics.width - metrics.blockRightMargin( blockIndex );
1810 break;
1811
1813 if ( !isFinalLineInParagraph && targetWidth > thisBlockMetrics.width )
1814 {
1815 calculateExtraSpacingForLineJustification( targetWidth - thisBlockMetrics.width, block, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace );
1816 thisBlockMetrics.width = targetWidth;
1817 }
1818 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1819 break;
1820
1822 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1823 break;
1824 }
1825
1826 switch ( mode )
1827 {
1832 thisBlockMetrics.xOffset = blockWidthDiff;
1833 break;
1834
1836 {
1837 switch ( blockAlignment )
1838 {
1840 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth;
1841 break;
1842
1844 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth / 2.0;
1845 break;
1846
1849 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1850 break;
1851 }
1852 }
1853 break;
1854 }
1855 }
1856 else if ( blockAlignment == Qgis::TextHorizontalAlignment::Left || blockAlignment == Qgis::TextHorizontalAlignment::Justify )
1857 {
1858 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1859 }
1860
1861 switch ( mode )
1862 {
1866 thisBlockMetrics.backgroundWidth = targetWidth;
1867 thisBlockMetrics.backgroundXOffset = 0;
1868 break;
1871 thisBlockMetrics.backgroundWidth = thisBlockMetrics.width;
1872 thisBlockMetrics.backgroundXOffset = thisBlockMetrics.xOffset;
1873 break;
1874 }
1875
1876 blockMetrics << thisBlockMetrics;
1877 blockIndex++;
1878 }
1879 return blockMetrics;
1880}
1881
1882QBrush QgsTextRenderer::createBrushForPath( QgsRenderContext &context, const QString &path )
1883{
1884 bool fitsInCache = false;
1885 // use original image size
1886 const QSize imageSize = QgsApplication::imageCache()->originalSize( path, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1887 // TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
1888 const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
1889 const double pixelsPerMm = context.scaleFactor();
1890 const double imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
1891 const double imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
1892 QBrush res;
1893 if ( imageWidth == 0 || imageHeight == 0 )
1894 return res;
1895 const QImage image = QgsApplication::imageCache()->pathAsImage( path,
1896 QSize( static_cast< int >( std::round( imageWidth ) ),
1897 static_cast< int >( std::round( imageHeight ) ) ),
1898 false,
1899 1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1900
1901 if ( !image.isNull() )
1902 {
1903
1904 res.setTextureImage( image );
1905 }
1906 return res;
1907}
1908
1909void 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 )
1910{
1911 int blockIndex = 0;
1912 context.painter()->translate( component.origin );
1913 if ( !qgsDoubleNear( rotation, 0.0 ) )
1914 context.painter()->rotate( rotation );
1915
1916 context.painter()->setPen( Qt::NoPen );
1917 context.painter()->setBrush( Qt::NoBrush );
1918 for ( const QgsTextBlock &block : document )
1919 {
1920 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1921 const double blockMaximumDescent = metrics.blockMaximumDescent( blockIndex );
1922 const double blockMaximumAscent = metrics.blockMaximumAscent( blockIndex );
1923
1924 if ( block.blockFormat().hasBackground() )
1925 {
1926 QBrush backgroundBrush = block.blockFormat().backgroundBrush();
1927 if ( !block.blockFormat().backgroundImagePath().isEmpty() )
1928 {
1929 const QBrush backgroundImageBrush = createBrushForPath( context, block.blockFormat().backgroundImagePath() );
1930 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1931 backgroundBrush = backgroundImageBrush;
1932 }
1933
1934 context.painter()->setBrush( backgroundBrush );
1935 context.painter()->drawRect( QRectF( blockMetrics[ blockIndex ].backgroundXOffset, baseLineOffset - blockMaximumAscent, blockMetrics[ blockIndex ].backgroundWidth, blockMaximumDescent + blockMaximumAscent ) );
1936 }
1937
1938 double xOffset = 0;
1939 int fragmentIndex = 0;
1940
1941 for ( const QgsTextFragment &fragment : block )
1942 {
1943 const double horizontalAdvance = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
1944 const double ascent = metrics.fragmentAscent( blockIndex, fragmentIndex, mode );
1945 const double descent = metrics.fragmentDescent( blockIndex, fragmentIndex, mode );
1946
1947 if ( fragment.characterFormat().hasBackground() )
1948 {
1949 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1950
1951 QBrush backgroundBrush = fragment.characterFormat().backgroundBrush();
1952 if ( !fragment.characterFormat().backgroundImagePath().isEmpty() )
1953 {
1954 const QBrush backgroundImageBrush = createBrushForPath( context, fragment.characterFormat().backgroundImagePath() );
1955 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1956 backgroundBrush = backgroundImageBrush;
1957 }
1958
1959 context.painter()->setBrush( backgroundBrush );
1960 context.painter()->drawRect( QRectF( blockMetrics[ blockIndex ].xOffset + xOffset,
1961 baseLineOffset + verticalAlignOffset + yOffset - ascent, horizontalAdvance, ascent + descent ) );
1962 }
1963
1964 xOffset += horizontalAdvance;
1965 fragmentIndex ++;
1966 }
1967
1968 blockIndex++;
1969 }
1970
1971 context.painter()->setBrush( Qt::NoBrush );
1972
1973 if ( !qgsDoubleNear( rotation, 0.0 ) )
1974 context.painter()->rotate( -rotation );
1975 context.painter()->translate( -component.origin );
1976}
1977
1978void 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,
1979 Qgis::TextVerticalAlignment vAlignment, double rotation )
1980{
1981 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1982
1983 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
1984
1985 double targetWidth = 0.0;
1986 switch ( mode )
1987 {
1990 targetWidth = documentSize.width();
1991 break;
1992
1996 targetWidth = component.size.width();
1997 break;
1998 }
1999
2000 double verticalAlignOffset = 0;
2001
2002 if ( mode == Qgis::TextLayoutMode::Rectangle )
2003 {
2004 const double overallHeight = documentSize.height();
2005 switch ( vAlignment )
2006 {
2008 verticalAlignOffset = metrics.blockVerticalMargin( - 1 );
2009 break;
2010
2012 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5 + metrics.blockVerticalMargin( - 1 );
2013 break;
2014
2016 verticalAlignOffset = ( component.size.height() - overallHeight ) + metrics.blockVerticalMargin( - 1 );
2017 break;
2018 }
2019 }
2020 else if ( mode == Qgis::TextLayoutMode::Point )
2021 {
2022 verticalAlignOffset = - metrics.blockVerticalMargin( document.size() - 1 );
2023 }
2024
2025 // should we use text or paths for this render?
2026 const bool usePathsForText = usePathsToRender( context, format, document );
2027
2028 // TODO -- maybe we can avoid the nested vector? Need to confirm whether painter rotation & translation can be
2029 // done ONCE only, upfront
2030 std::unique_ptr< std::vector< DeferredRenderBlock > > deferredBlocks;
2031
2032 // Depending on format settings, we may need to render in multiple passes. Eg buffer than text, or shadow than text.
2033 // We try to avoid this if possible as it requires more work, and just do a single pass, rendering text directly as we go.
2034 // If we need to do multi-pass rendering then we'll calculate paths ONCE upfront and defer actually renderring these.
2035 const bool requiresMultiPassRendering = ( components & Qgis::TextComponent::Buffer && format.buffer().enabled() )
2037 if ( requiresMultiPassRendering )
2038 {
2039 deferredBlocks = std::make_unique< std::vector< DeferredRenderBlock > >();
2040 deferredBlocks->reserve( document.size() );
2041 }
2042
2043 if ( ( components & Qgis::TextComponent::Buffer )
2044 || ( components & Qgis::TextComponent::Text )
2045 || ( components & Qgis::TextComponent::Shadow ) )
2046 {
2047 const QVector< BlockMetrics > blockMetrics = calculateBlockMetrics( document, metrics, mode, targetWidth, hAlignment );
2048
2049 if ( document.hasBackgrounds() )
2050 {
2051 renderDocumentBackgrounds( context, document, metrics, component, blockMetrics, mode, verticalAlignOffset, rotation );
2052 }
2053
2054 int blockIndex = 0;
2055 for ( const QgsTextBlock &block : document )
2056 {
2057 const double blockHeight = metrics.blockHeight( blockIndex );
2058
2059 DeferredRenderBlock *deferredBlock = nullptr;
2060 if ( requiresMultiPassRendering && deferredBlocks )
2061 {
2062 deferredBlocks->emplace_back( DeferredRenderBlock() );
2063 deferredBlock = &deferredBlocks->back();
2064 deferredBlock->fragments.reserve( block.size() );
2065 }
2066
2067 QgsScopedQPainterState painterState( context.painter() );
2069 context.painter()->translate( component.origin );
2070 if ( !qgsDoubleNear( rotation, 0.0 ) )
2071 context.painter()->rotate( rotation );
2072
2073 // apply to the mask painter the same transformations
2074 if ( maskPainter )
2075 {
2076 maskPainter->save();
2077 maskPainter->translate( component.origin );
2078 if ( !qgsDoubleNear( rotation, 0.0 ) )
2079 maskPainter->rotate( rotation );
2080 }
2081
2082 const BlockMetrics thisBlockMetrics = blockMetrics[ blockIndex ];
2083 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
2084
2085 const QPointF blockOrigin( thisBlockMetrics.xOffset, baseLineOffset + verticalAlignOffset );
2086 if ( deferredBlock )
2087 deferredBlock->origin = blockOrigin;
2088 else
2089 context.painter()->translate( blockOrigin );
2090 if ( maskPainter )
2091 maskPainter->translate( blockOrigin );
2092
2093 Component subComponent;
2094 subComponent.block = block;
2095 subComponent.blockIndex = blockIndex;
2096 subComponent.size = QSizeF( thisBlockMetrics.width, blockHeight );
2097 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
2098 subComponent.rotation = -component.rotation * 180 / M_PI;
2099 subComponent.rotationOffset = 0.0;
2100 subComponent.extraWordSpacing = thisBlockMetrics.extraWordSpace * fontScale;
2101 subComponent.extraLetterSpacing = thisBlockMetrics.extraLetterSpace * fontScale;
2102 if ( deferredBlock )
2103 deferredBlock->component = subComponent;
2104
2105 // draw the mask below the text (for preview)
2106 if ( format.mask().enabled() )
2107 {
2108 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
2109 }
2110
2111 // if we are drawing both text + buffer, we'll need a path, as we HAVE to render buffers using paths
2112 const bool needsPaths = usePathsForText
2113 || ( ( components & Qgis::TextComponent::Buffer ) && format.buffer().enabled() )
2114 || ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() );
2115
2116 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2117 if ( mode == Qgis::TextLayoutMode::Labeling )
2118 {
2119 // label size has already been calculated using any symbology reference scale factor -- we need
2120 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2121 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2122 }
2123
2124 referenceScaleOverride.reset();
2125
2126 // now render the actual text
2127 if ( context.useAdvancedEffects() )
2128 {
2129 context.painter()->setCompositionMode( format.blendMode() );
2130 }
2131
2132 // scale for any print output or image saving @ specific dpi
2133 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2134
2135 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2136 context.painter()->setPen( Qt::NoPen );
2137 context.painter()->setBrush( Qt::NoBrush );
2138
2139 renderBlockHorizontal( block, blockIndex, metrics, context, format, context.painter(), needsPaths,
2140 fontScale, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace, mode, deferredBlock );
2141
2142 if ( maskPainter )
2143 maskPainter->restore();
2144
2145 blockIndex++;
2146 }
2147 }
2148
2149 if ( deferredBlocks )
2150 {
2151 renderDeferredBlocks(
2152 context, format, components, *deferredBlocks, usePathsForText, fontScale, component, rotation
2153 );
2154 }
2155}
2156
2157void QgsTextRenderer::renderDeferredBlocks( QgsRenderContext &context,
2158 const QgsTextFormat &format,
2159 Qgis::TextComponents components,
2160 const std::vector< DeferredRenderBlock > &deferredBlocks,
2161 bool usePathsForText,
2162 double fontScale,
2163 const Component &component,
2164 double rotation )
2165{
2166 if ( format.buffer().enabled() && ( components & Qgis::TextComponent::Buffer ) )
2167 {
2168 renderDeferredBuffer( context, format, components, deferredBlocks, fontScale, component, rotation );
2169 }
2170
2171 if ( ( components & Qgis::TextComponent::Shadow )
2172 && format.shadow().enabled()
2174 {
2175 renderDeferredShadowForText( context, format, deferredBlocks, fontScale, component, rotation );
2176 // TODO: there's an optimisation opportunity here -- if we are ALSO rendering the text component,
2177 // we could move the actual text rendering into renderDeferredShadowForText and use the same
2178 // QPicture as we used for the shadow. But we'd need to ensure that all the settings
2179 // which control whether text is rendered as text or paths also also considered.
2180 }
2181
2182 if ( components & Qgis::TextComponent::Text )
2183 {
2184 renderDeferredText( context, deferredBlocks, usePathsForText, fontScale, component, rotation );
2185 }
2186}
2187
2188void QgsTextRenderer::renderDeferredShadowForText( QgsRenderContext &context,
2189 const QgsTextFormat &format,
2190 const std::vector< DeferredRenderBlock > &deferredBlocks,
2191 double fontScale,
2192 const Component &component,
2193 double rotation )
2194{
2195 QgsScopedQPainterState painterState( context.painter() );
2197 context.painter()->translate( component.origin );
2198 if ( !qgsDoubleNear( rotation, 0.0 ) )
2199 context.painter()->rotate( rotation );
2200
2201 context.painter()->setPen( Qt::NoPen );
2202 context.painter()->setBrush( Qt::NoBrush );
2203
2204 for ( const DeferredRenderBlock &block : deferredBlocks )
2205 {
2206 Component subComponent = block.component;
2207
2208 QPainter painter( &subComponent.picture );
2209 painter.setPen( Qt::NoPen );
2210 painter.setBrush( Qt::NoBrush );
2211 painter.scale( 1 / fontScale, 1 / fontScale );
2212
2213 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2214 {
2215 if ( !fragment.path.isEmpty() )
2216 {
2217 painter.setBrush( fragment.color );
2218 painter.drawPath( fragment.path );
2219 }
2220 else
2221 {
2222 painter.setPen( fragment.color );
2223 painter.setFont( fragment.font );
2224 painter.drawText( fragment.point, fragment.text );
2225 }
2226 }
2227 painter.end();
2228
2229 subComponent.pictureBuffer = 1.0; // no pen width to deal with, but we'll add 1 px for antialiasing
2230 subComponent.origin = QPointF( 0.0, 0.0 );
2231 const QRectF pictureBoundingRect = subComponent.picture.boundingRect();
2232 subComponent.size = pictureBoundingRect.size();
2233 subComponent.offset = QPointF( -pictureBoundingRect.left(), -pictureBoundingRect.height() - pictureBoundingRect.top() );
2234
2235 context.painter()->translate( block.origin );
2236 drawShadow( context, subComponent, format );
2237 context.painter()->translate( -block.origin );
2238 }
2239}
2240
2241void QgsTextRenderer::renderDeferredBuffer( QgsRenderContext &context,
2242 const QgsTextFormat &format,
2243 Qgis::TextComponents components,
2244 const std::vector< DeferredRenderBlock > &deferredBlocks,
2245 double fontScale,
2246 const Component &component,
2247 double rotation )
2248{
2249 QgsScopedQPainterState painterState( context.painter() );
2251
2252 // 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
2253 // to generate the shadow, and then render the QPicture as the buffer on top. If not, avoid the unwanted expense of the temporary QPicture
2254 // and render directly.
2255 const bool needsShadowOnBuffer = ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer );
2256 std::unique_ptr< QPicture > bufferPicture;
2257 std::unique_ptr< QPainter > bufferPainter;
2258 QPainter *prevPainter = context.painter();
2259 if ( needsShadowOnBuffer )
2260 {
2261 bufferPicture = std::make_unique< QPicture >();
2262 bufferPainter = std::make_unique< QPainter >( bufferPicture.get() );
2263 context.setPainter( bufferPainter.get() );
2264 }
2265
2266 std::unique_ptr< QgsPaintEffect > tmpEffect;
2267 if ( format.buffer().paintEffect() && format.buffer().paintEffect()->enabled() )
2268 {
2269 tmpEffect.reset( format.buffer().paintEffect()->clone() );
2270 tmpEffect->begin( context );
2271 }
2272
2273 QColor bufferColor = format.buffer().color();
2274 bufferColor.setAlphaF( format.buffer().opacity() );
2275 QPen pen( bufferColor );
2276 const QgsTextBufferSettings &buffer = format.buffer();
2277 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage
2278 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
2279 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
2280 pen.setWidthF( penSize * fontScale );
2281 pen.setJoinStyle( buffer.joinStyle() );
2282 context.painter()->setPen( pen );
2283
2284 // honor pref for whether to fill buffer interior
2285 if ( !buffer.fillBufferInterior() )
2286 {
2287 bufferColor.setAlpha( 0 );
2288 }
2289 context.painter()->setBrush( bufferColor );
2290
2291 context.painter()->translate( component.origin );
2292 if ( !qgsDoubleNear( rotation, 0.0 ) )
2293 context.painter()->rotate( rotation );
2294
2295 if ( context.useAdvancedEffects() )
2296 {
2297 context.painter()->setCompositionMode( format.buffer().blendMode() );
2298 }
2299
2300 for ( const DeferredRenderBlock &block : deferredBlocks )
2301 {
2302 context.painter()->translate( block.origin );
2303 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2304 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2305 {
2306 context.painter()->drawPath( fragment.path );
2307 }
2308 context.painter()->scale( fontScale, fontScale );
2309 context.painter()->translate( -block.origin );
2310 }
2311
2312 if ( tmpEffect )
2313 {
2314 tmpEffect->end( context );
2315 }
2316
2317 if ( needsShadowOnBuffer && bufferPicture )
2318 {
2319 bufferPainter->end();
2320 bufferPainter.reset();
2321 context.setPainter( prevPainter );
2322
2323 QgsTextRenderer::Component bufferComponent = component;
2324 bufferComponent.origin = QPointF( 0.0, 0.0 );
2325 bufferComponent.picture = *bufferPicture;
2326 bufferComponent.pictureBuffer = penSize / 2.0;
2327 const QRectF bufferBoundingBox = bufferPicture->boundingRect();
2328 bufferComponent.size = bufferBoundingBox.size();
2329 bufferComponent.offset = QPointF( -bufferBoundingBox.left(), -bufferBoundingBox.height() - bufferBoundingBox.top() );
2330
2331 drawShadow( context, bufferComponent, format );
2332
2333 // also draw buffer
2334 if ( context.useAdvancedEffects() )
2335 {
2336 context.painter()->setCompositionMode( buffer.blendMode() );
2337 }
2338
2339 // scale for any print output or image saving @ specific dpi
2340 context.painter()->scale( component.dpiRatio, component.dpiRatio );
2341 QgsPainting::drawPicture( context.painter(), QPointF( 0, 0 ), *bufferPicture );
2342 }
2343}
2344
2345void QgsTextRenderer::renderDeferredText( QgsRenderContext &context,
2346 const std::vector< DeferredRenderBlock > &deferredBlocks,
2347 bool usePathsForText,
2348 double fontScale,
2349 const Component &component,
2350 double rotation )
2351{
2352 QgsScopedQPainterState painterState( context.painter() );
2354 context.painter()->translate( component.origin );
2355 if ( !qgsDoubleNear( rotation, 0.0 ) )
2356 context.painter()->rotate( rotation );
2357
2358 context.painter()->setPen( Qt::NoPen );
2359 context.painter()->setBrush( Qt::NoBrush );
2360
2361 // draw the text
2362 for ( const DeferredRenderBlock &block : deferredBlocks )
2363 {
2364 context.painter()->translate( block.origin );
2365 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2366
2367 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2368 {
2369 if ( usePathsForText )
2370 {
2371 context.painter()->setBrush( fragment.color );
2372 context.painter()->drawPath( fragment.path );
2373 }
2374 else
2375 {
2376 context.painter()->setPen( fragment.color );
2377 context.painter()->setFont( fragment.font );
2378 context.painter()->drawText( fragment.point, fragment.text );
2379 }
2380 }
2381
2382 context.painter()->scale( fontScale, fontScale );
2383 context.painter()->translate( -block.origin );
2384 }
2385}
2386
2387void 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 )
2388{
2389 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2390 const QStringList textLines = document.toPlainText();
2391
2392 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2393 if ( mode == Qgis::TextLayoutMode::Labeling )
2394 {
2395 // label size has already been calculated using any symbology reference scale factor -- we need
2396 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2397 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2398 }
2399
2400 if ( metrics.isNullFontSize() )
2401 return;
2402
2403 referenceScaleOverride.reset();
2404
2405 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
2406 const double actualTextWidth = documentSize.width();
2407 double textRectWidth = 0.0;
2408
2409 switch ( mode )
2410 {
2413 textRectWidth = actualTextWidth;
2414 break;
2415
2419 textRectWidth = component.size.width();
2420 break;
2421 }
2422
2423 int maxLineLength = 0;
2424 for ( const QString &line : std::as_const( textLines ) )
2425 {
2426 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2427 }
2428
2429 const double actualLabelHeight = documentSize.height();
2430 int blockIndex = 0;
2431
2432 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2433
2434 for ( const QgsTextBlock &block : document )
2435 {
2436 QgsScopedQPainterState painterState( context.painter() );
2438
2439 context.painter()->translate( component.origin );
2440 if ( !qgsDoubleNear( rotation, 0.0 ) )
2441 context.painter()->rotate( rotation );
2442
2443 // apply to the mask painter the same transformations
2444 if ( maskPainter )
2445 {
2446 maskPainter->save();
2447 maskPainter->translate( component.origin );
2448 if ( !qgsDoubleNear( rotation, 0.0 ) )
2449 maskPainter->rotate( rotation );
2450 }
2451
2452 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2453
2454 // figure x offset of multiple lines
2455 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2456 if ( adjustForAlignment )
2457 {
2458 double hAlignmentOffset = 0;
2459 switch ( hAlignment )
2460 {
2462 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2463 break;
2464
2466 hAlignmentOffset = textRectWidth - actualTextWidth;
2467 break;
2468
2471 break;
2472 }
2473
2474 switch ( mode )
2475 {
2480 xOffset += hAlignmentOffset;
2481 break;
2482
2484 break;
2485 }
2486 }
2487
2488 double yOffset = 0.0;
2489 switch ( mode )
2490 {
2493 {
2494 if ( rotation >= -405 && rotation < -180 )
2495 {
2496 yOffset = 0;
2497 }
2498 else if ( rotation >= 0 && rotation < 45 )
2499 {
2500 xOffset -= actualTextWidth;
2501 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2502 }
2503 }
2504 else
2505 {
2506 yOffset = -actualLabelHeight;
2507 }
2508 break;
2509
2511 yOffset = -actualLabelHeight;
2512 break;
2513
2517 yOffset = 0;
2518 break;
2519 }
2520
2521 context.painter()->translate( QPointF( xOffset, yOffset ) );
2522
2523 double currentBlockYOffset = 0;
2524 int fragmentIndex = 0;
2525 for ( const QgsTextFragment &fragment : block )
2526 {
2527 QgsScopedQPainterState fragmentPainterState( context.painter() );
2528
2529 // apply some character replacement to draw symbols in vertical presentation
2530 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2531
2532 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2533
2534 QFontMetricsF fragmentMetrics( fragmentFont );
2535
2536 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2537 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2538
2539 Component subComponent;
2540 subComponent.block = QgsTextBlock( fragment );
2541 subComponent.blockIndex = blockIndex;
2542 subComponent.firstFragmentIndex = fragmentIndex;
2543 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2544 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2545 subComponent.rotation = -component.rotation * 180 / M_PI;
2546 subComponent.rotationOffset = 0.0;
2547
2548 // draw the mask below the text (for preview)
2549 if ( format.mask().enabled() )
2550 {
2551 // WARNING: totally broken! (has been since mask was introduced)
2552#if 0
2553 QgsTextRenderer::drawMask( context, subComponent, format );
2554#endif
2555 }
2556
2557 if ( components & Qgis::TextComponent::Buffer )
2558 {
2559 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2560 }
2561 if ( ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2562 {
2563 // draw text, QPainterPath method
2564 QPainterPath path;
2565 path.setFillRule( Qt::WindingFill );
2566 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2567 double partYOffset = 0.0;
2568 for ( const QString &part : parts )
2569 {
2570 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2571 partYOffset += fragmentMetrics.ascent() / fontScale;
2572 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2573 partYOffset += letterSpacing;
2574 }
2575
2576 // store text's drawing in QPicture for drop shadow call
2577 QPicture textPict;
2578 QPainter textp;
2579 textp.begin( &textPict );
2580 textp.setPen( Qt::NoPen );
2581 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2582 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2583 textp.setBrush( textColor );
2584 textp.scale( 1 / fontScale, 1 / fontScale );
2585 textp.drawPath( path );
2586
2587 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2588 // e.g. some capitalization options, but not others
2589 //textp.setFont( tmpLyr.textFont );
2590 //textp.setPen( tmpLyr.textColor );
2591 //textp.drawText( 0, 0, component.text() );
2592 textp.end();
2593
2594 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2595 {
2596 subComponent.picture = textPict;
2597 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2598 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2599 const double prevY = subComponent.offset.y();
2600 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2601 subComponent.useOrigin = true;
2602 QgsTextRenderer::drawShadow( context, subComponent, format );
2603 subComponent.useOrigin = false;
2604 subComponent.offset = QPointF( 0, prevY );
2605 }
2606
2607 // paint the text
2608 if ( context.useAdvancedEffects() )
2609 {
2610 context.painter()->setCompositionMode( format.blendMode() );
2611 }
2612
2613 // scale for any print output or image saving @ specific dpi
2614 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2615
2616 // TODO -- this should respect the context's TextRenderFormat
2617 // draw outlined text
2618 context.painter()->translate( 0, currentBlockYOffset );
2620 context.painter()->drawPicture( 0, 0, textPict );
2621 currentBlockYOffset += partYOffset;
2622 }
2623 fragmentIndex++;
2624 }
2625
2626 if ( maskPainter )
2627 maskPainter->restore();
2628 blockIndex++;
2629 }
2630}
2631
2633{
2635 return 1.0;
2636
2637 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2638
2639 // THESE THRESHOLDS MAY NEED TWEAKING!
2640
2641 // NOLINTBEGIN(bugprone-branch-clone)
2642
2643 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2644 // 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
2645 if ( pixelSize < 50 )
2646 return 200 / pixelSize;
2647 //... 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!
2648 // 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
2649 else if ( pixelSize > 200 )
2650 return 200 / pixelSize;
2651 else
2652 return 1.0;
2653
2654 // NOLINTEND(bugprone-branch-clone)
2655}
2656
TextLayoutMode
Text layout modes.
Definition qgis.h:2699
@ Labeling
Labeling-specific layout mode.
@ Point
Text at point of origin layout mode.
@ RectangleAscentBased
Similar to Rectangle mode, but uses ascents only when calculating font and line heights.
@ RectangleCapHeightBased
Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line ...
@ Rectangle
Text within rectangle layout mode.
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3162
TextOrientation
Text orientations.
Definition qgis.h:2684
@ Vertical
Vertically oriented text.
@ RotationBased
Horizontally or vertically oriented text based on rotation (only available for map labeling)
@ Horizontal
Horizontally oriented text.
@ Round
Use rounded joins.
@ Normal
Adjacent characters are positioned in the standard way for text in the writing system in use.
@ SubScript
Characters are placed below the base line for normal text.
@ SuperScript
Characters are placed above the base line for normal text.
@ PreferText
Render text as text objects, unless doing so results in rendering artifacts or poor quality rendering...
@ AlwaysOutlines
Always render text using path objects (AKA outlines/curves). This setting guarantees the best quality...
@ AlwaysText
Always render text as text objects. While this mode preserves text objects as text for post-processin...
RenderUnit
Rendering size units.
Definition qgis.h:4847
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size)
@ Unknown
Mixed or unknown units.
@ MapUnits
Map units.
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
@ RenderBlocking
Render and load remote sources in the same thread to ensure rendering remote sources (svg and images)...
TextVerticalAlignment
Text vertical alignment.
Definition qgis.h:2759
@ Bottom
Align to bottom.
@ VerticalCenter
Center align.
QFlags< TextComponent > TextComponents
Text components.
Definition qgis.h:2729
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:2740
@ WrapLines
Automatically wrap long lines of text.
TextComponent
Text components.
Definition qgis.h:2716
@ Shadow
Drop shadow.
@ Buffer
Buffer component.
@ Text
Text component.
@ Background
Background shape.
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
A class to manager painter saving and restoring required for effect drawing.
QgsFeature feature() const
Convenience function for retrieving the feature for the context, if set.
QgsFields fields() const
Convenience function for retrieving the fields for the context, if set.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:58
QgsFillSymbol * clone() const override
Returns a deep copy of this symbol.
Does vector analysis using the geos library and handles import, export, exception handling*.
Definition qgsgeos.h:137
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, double targetDpi=96, int frameNumber=-1, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
Line string geometry type, with support for z-dimension and m-values.
static QgsLineString * fromQPolygonF(const QPolygonF &polygon)
Returns a new linestring from a QPolygonF polygon input.
Struct for storing maximum and minimum scales for measurements in map units.
A marker symbol type, for rendering Point and MultiPoint geometries.
QgsMarkerSymbol * clone() const override
Returns a deep copy of this symbol.
bool enabled() const
Returns whether the effect is enabled.
virtual QgsPaintEffect * clone() const =0
Duplicates an effect by creating a deep copy of the effect.
A class to manage painter saving and restoring required for drawing on a different painter (mask pain...
static void applyScaleFixForQPictureDpi(QPainter *painter)
Applies a workaround to a painter to avoid an issue with incorrect scaling when drawing QPictures.
static void drawPicture(QPainter *painter, const QPointF &point, const QPicture &picture)
Draws a picture onto a painter, correctly applying workarounds to avoid issues with incorrect scaling...
static QStringList splitToGraphemes(const QString &text)
Splits a text string to a list of graphemes, which are the smallest allowable character divisions in ...
Contains precalculated properties regarding text metrics for text to be renderered at a later stage.
void setGraphemeFormats(const QVector< QgsTextCharacterFormat > &formats)
Sets the character formats associated with the text graphemes().
bool hasActiveProperties() const final
Returns true if the collection has any active properties, or false if all properties within the colle...
Contains information about the context of a rendering operation.
double scaleFactor() const
Returns the scaling factor for the render to convert painter units to physical sizes.
bool useAdvancedEffects() const
Returns true if advanced effects such as blend modes such be used.
void setScaleFactor(double factor)
Sets the scaling factor for the render to convert painter units to physical sizes.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
QgsExpressionContext & expressionContext()
Gets the expression context.
bool isGuiPreview() const
Returns the Gui preview mode.
Qgis::TextRenderFormat textRenderFormat() const
Returns the text render format, which dictates how text is rendered (e.g.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
QPainter * maskPainter(int id=0)
Returns a mask QPainter for the render operation.
void setMapToPixel(const QgsMapToPixel &mtp)
Sets the context's map to pixel transform, which transforms between map coordinates and device coordi...
int currentMaskId() const
Returns the current mask id, which can be used with maskPainter()
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
Qgis::RenderContextFlags flags() const
Returns combination of flags used for rendering.
Scoped object for saving and restoring a QPainter object's state.
Scoped object for temporary override of the symbologyReferenceScale property of a QgsRenderContext.
static QString substituteVerticalCharacters(QString string)
Returns a string with characters having vertical representation form substituted.
static QgsSymbolLayer * create(const QVariantMap &properties=QVariantMap())
Creates the symbol.
void renderPoint(QPointF point, QgsSymbolRenderContext &context) override
Renders a marker at the specified point.
static void blurImageInPlace(QImage &image, QRect rect, int radius, bool alphaOnly)
Blurs an image in place, e.g. creating Qt-independent drop shadows.
static double estimateMaxSymbolBleed(QgsSymbol *symbol, const QgsRenderContext &context)
Returns the maximum estimated bleed for the symbol.
Container for settings relating to a text background object.
QgsMapUnitScale strokeWidthMapUnitScale() const
Returns the map unit scale object for the shape stroke width.
RotationType rotationType() const
Returns the method used for rotating the background shape.
QString svgFile() const
Returns the absolute path to the background SVG file, if set.
QSizeF size() const
Returns the size of the background shape.
QSizeF radii() const
Returns the radii used for rounding the corners of shapes.
QgsMapUnitScale radiiMapUnitScale() const
Returns the map unit scale object for the shape radii.
Qgis::RenderUnit radiiUnit() const
Returns the units used for the shape's radii.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the background shape.
@ SizeBuffer
Shape size is determined by adding a buffer margin around text.
bool enabled() const
Returns whether the background is enabled.
double opacity() const
Returns the background shape's opacity.
double rotation() const
Returns the rotation for the background shape, in degrees clockwise.
QColor fillColor() const
Returns the color used for filing the background shape.
SizeType sizeType() const
Returns the method used to determine the size of the background shape (e.g., fixed size or buffer aro...
Qgis::RenderUnit strokeWidthUnit() const
Returns the units used for the shape's stroke width.
ShapeType type() const
Returns the type of background shape (e.g., square, ellipse, SVG).
double strokeWidth() const
Returns the width of the shape's stroke (stroke).
@ ShapeSquare
Square - buffered sizes only.
Qgis::RenderUnit offsetUnit() const
Returns the units used for the shape's offset.
QColor strokeColor() const
Returns the color used for outlining the background shape.
QgsFillSymbol * fillSymbol() const
Returns the fill symbol to be rendered in the background.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the shape size.
Qgis::RenderUnit sizeUnit() const
Returns the units used for the shape's size.
@ RotationOffset
Shape rotation is offset from text rotation.
@ RotationFixed
Shape rotation is a fixed angle.
QgsMarkerSymbol * markerSymbol() const
Returns the marker symbol to be rendered in the background.
const QgsPaintEffect * paintEffect() const
Returns the current paint effect for the background shape.
QgsMapUnitScale offsetMapUnitScale() const
Returns the map unit scale object for the shape offset.
QPointF offset() const
Returns the offset used for drawing the background shape.
Qgis::TextHorizontalAlignment horizontalAlignment() const
Returns the format horizontal alignment.
bool hasBackground() const
Returns true if the block has a background set.
QBrush backgroundBrush() const
Returns the brush used for rendering the background of the block.
QString backgroundImagePath() const
Returns the path for the image to be used for rendering the background of the fragment.
bool hasHorizontalAlignmentSet() const
Returns true if the format has an explicit horizontal alignment set.
Represents a block of text consisting of one or more QgsTextFragment objects.
int size() const
Returns the number of fragments in the block.
QString toPlainText() const
Converts the block to plain text.
const QgsTextBlockFormat & blockFormat() const
Returns the block formatting for the fragment.
Container for settings relating to a text buffer.
Qgis::RenderUnit sizeUnit() const
Returns the units for the buffer size.
Qt::PenJoinStyle joinStyle() const
Returns the buffer join style.
double size() const
Returns the size of the buffer.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the buffer size.
bool enabled() const
Returns whether the buffer is enabled.
double opacity() const
Returns the buffer opacity.
bool fillBufferInterior() const
Returns whether the interior of the buffer will be filled in.
const QgsPaintEffect * paintEffect() const
Returns the current paint effect for the buffer.
QColor color() const
Returns the color of the buffer.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the buffer.
Stores information relating to individual character formatting.
void updateFontForFormat(QFont &font, const QgsRenderContext &context, double scaleFactor=1.0) const
Updates the specified font in place, applying character formatting options which are applicable on a ...
Qgis::TextCharacterVerticalAlignment verticalAlignment() const
Returns the format vertical alignment.
bool hasVerticalAlignmentSet() const
Returns true if the format has an explicit vertical alignment set.
double fontPointSize() const
Returns the font point size, or -1 if the font size is not set and should be inherited.
Contains pre-calculated metrics of a QgsTextDocument.
double verticalOrientationXOffset(int blockIndex) const
Returns the vertical orientation x offset for the specified block.
double fragmentVerticalOffset(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the vertical offset from a text block's baseline which should be applied to the fragment at t...
double blockMaximumDescent(int blockIndex) const
Returns the maximum descent encountered in the specified block.
double fragmentDescent(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the descent of the fragment at the specified block and fragment index.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
double blockRightMargin(int blockIndex) const
Returns the margin for the right side of the specified block index.
static QgsTextDocumentMetrics calculateMetrics(const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor=1.0, const QgsTextDocumentRenderContext &documentContext=QgsTextDocumentRenderContext())
Returns precalculated text metrics for a text document, when rendered using the given base format and...
QFont fragmentFont(int blockIndex, int fragmentIndex) const
Returns the calculated font for the fragment at the specified block and fragment indices.
double blockMaximumCharacterWidth(int blockIndex) const
Returns the maximum character width for the specified block.
double baselineOffset(int blockIndex, Qgis::TextLayoutMode mode) const
Returns the offset from the top of the document to the text baseline for the given block index.
double fragmentFixedHeight(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the fixed height of the fragment at the specified block and fragment index,...
double blockLeftMargin(int blockIndex) const
Returns the margin for the left side of the specified block index.
double blockMaximumAscent(int blockIndex) const
Returns the maximum ascent encountered in the specified block.
double fragmentAscent(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the ascent of the fragment at the specified block and fragment index.
double blockHeight(int blockIndex) const
Returns the height of the block at the specified index.
double fragmentHorizontalAdvance(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the horizontal advance of the fragment at the specified block and fragment index.
bool isNullFontSize() const
Returns true if the metrics could not be calculated because the text format has a null font size.
const QgsTextDocument & document() const
Returns the document associated with the calculated metrics.
double blockWidth(int blockIndex) const
Returns the width of the block at the specified index.
double ascentOffset() const
Returns the ascent offset of the first block in the document.
double blockVerticalMargin(int blockIndex) const
Returns the vertical margin for the specified block index.
Encapsulates the context in which a text document is to be rendered.
void setFlags(Qgis::TextRendererFlags flags)
Sets associated text renderer flags.
void setMaximumWidth(double width)
Sets the maximum width (in painter units) for rendered text.
Represents a document consisting of one or more QgsTextBlock objects.
const QgsTextBlock & at(int index) const
Returns the block at the specified index.
QStringList toPlainText() const
Returns a list of plain text lines of text representing the document.
int size() const
Returns the number of blocks in the document.
void append(const QgsTextBlock &block)
Appends a block to the document.
static QgsTextDocument fromTextAndFormat(const QStringList &lines, const QgsTextFormat &format)
Constructor for QgsTextDocument consisting of a set of lines, respecting settings from a text format.
void applyCapitalization(Qgis::Capitalization capitalization)
Applies a capitalization style to the document's text.
bool hasBackgrounds() const
Returns true if any blocks or fragments in the document have background brushes set.
Container for all settings relating to text rendering.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the size.
void updateDataDefinedProperties(QgsRenderContext &context)
Updates the format by evaluating current values of data defined properties.
QgsPropertyCollection & dataDefinedProperties()
Returns a reference to the format's property collection, used for data defined overrides.
QFont scaledFont(const QgsRenderContext &context, double scaleFactor=1.0, bool *isZeroSize=nullptr) const
Returns a font with the size scaled to match the format's size settings (including units and map unit...
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the text.
Qgis::Capitalization capitalization() const
Returns the text capitalization style.
QgsTextMaskSettings & mask()
Returns a reference to the masking settings.
QgsTextBackgroundSettings & background()
Returns a reference to the text background settings.
Qgis::RenderUnit sizeUnit() const
Returns the units for the size of rendered text.
double opacity() const
Returns the text's opacity.
Qgis::TextOrientation orientation() const
Returns the orientation of the text.
double size() const
Returns the size for rendered text.
QgsTextShadowSettings & shadow()
Returns a reference to the text drop shadow settings.
QColor color() const
Returns the color that text will be rendered in.
QFont font() const
Returns the font used for rendering text.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.
Container for settings relating to a selective masking around a text.
Qgis::RenderUnit sizeUnit() const
Returns the units for the buffer size.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the buffer size.
double size() const
Returns the size of the buffer.
QgsPaintEffect * paintEffect() const
Returns the current paint effect for the mask.
double opacity() const
Returns the mask's opacity.
bool enabled() const
Returns whether the mask is enabled.
Qt::PenJoinStyle joinStyle() const
Returns the buffer join style.
Contains placement information for a single grapheme in a curved text layout.
@ RespectPainterOrientation
Curved text will be placed respecting the painter orientation, and the actual line direction will be ...
@ TruncateStringWhenLineIsTooShort
When a string is too long for the line, truncate characters instead of aborting the placement.
@ UseBaselinePlacement
Generate placement based on the character baselines instead of centers.
static std::unique_ptr< CurvePlacementProperties > generateCurvedTextPlacement(const QgsPrecalculatedTextMetrics &metrics, const QPolygonF &line, double offsetAlongLine, LabelLineDirection direction=RespectPainterOrientation, double maxConcaveAngle=-1, double maxConvexAngle=-1, CurvedTextFlags flags=CurvedTextFlags())
Calculates curved text placement properties.
static void drawDocumentOnLine(const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine=0, double offsetFromLine=0)
Draws a text document along a line using the specified settings.
static Qgis::TextVerticalAlignment convertQtVAlignment(Qt::Alignment alignment)
Converts a Qt vertical alignment flag to a Qgis::TextVerticalAlignment value.
static double textWidth(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF *fontMetrics=nullptr)
Returns the width of a text based on a given format.
static void drawDocument(const QRectF &rect, const QgsTextFormat &format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment horizontalAlignment=Qgis::TextHorizontalAlignment::Left, Qgis::TextVerticalAlignment verticalAlignment=Qgis::TextVerticalAlignment::Top, double rotation=0, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags())
Draws a text document within a rectangle using the specified settings.
static int sizeToPixel(double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale=QgsMapUnitScale())
Calculates pixel size (considering output size should be in pixel or map units, scale factors and opt...
static Q_DECL_DEPRECATED void drawPart(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool drawAsOutlines=true)
Draws a single component of rendered text using the specified settings.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
static bool textRequiresWrapping(const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format)
Returns true if the specified text requires line wrapping in order to fit within the specified width ...
static QFontMetricsF fontMetrics(QgsRenderContext &context, const QgsTextFormat &format, double scaleFactor=1.0)
Returns the font metrics for the given text format, when rendered in the specified render context.
static void drawTextOnLine(const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &format, double offsetAlongLine=0, double offsetFromLine=0)
Draws text along a line using the specified settings.
static double calculateScaleFactorForFormat(const QgsRenderContext &context, const QgsTextFormat &format)
Returns the scale factor used for upscaling font sizes and downscaling destination painter devices.
static QStringList wrappedText(const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format)
Wraps a text string to multiple lines, such that each individual line will fit within the specified w...
static double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Point, QFontMetricsF *fontMetrics=nullptr, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), double maxLineWidth=0)
Returns the height of a text based on a given format.
static constexpr double SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR
Scale factor to use for super or subscript text which doesn't have an explicit font size set.
static Qgis::TextHorizontalAlignment convertQtHAlignment(Qt::Alignment alignment)
Converts a Qt horizontal alignment flag to a Qgis::TextHorizontalAlignment value.
Container for settings relating to a text shadow.
int offsetAngle() const
Returns the angle for offsetting the position of the shadow from the text.
bool enabled() const
Returns whether the shadow is enabled.
int scale() const
Returns the scaling used for the drop shadow (in percentage of original size).
Qgis::RenderUnit offsetUnit() const
Returns the units used for the shadow's offset.
void setShadowPlacement(QgsTextShadowSettings::ShadowPlacement placement)
Sets the placement for the drop shadow.
double opacity() const
Returns the shadow's opacity.
QgsMapUnitScale blurRadiusMapUnitScale() const
Returns the map unit scale object for the shadow blur radius.
QColor color() const
Returns the color of the drop shadow.
@ ShadowBuffer
Draw shadow under buffer.
@ ShadowShape
Draw shadow under background shape.
@ ShadowLowest
Draw shadow below all text components.
@ ShadowText
Draw shadow under text.
QgsTextShadowSettings::ShadowPlacement shadowPlacement() const
Returns the placement for the drop shadow.
Qgis::RenderUnit blurRadiusUnit() const
Returns the units used for the shadow's blur radius.
double offsetDistance() const
Returns the distance for offsetting the position of the shadow from the text.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the drop shadow.
QgsMapUnitScale offsetMapUnitScale() const
Returns the map unit scale object for the shadow offset distance.
bool blurAlphaOnly() const
Returns whether only the alpha channel for the shadow will be blurred.
bool offsetGlobal() const
Returns true if the global shadow offset will be used.
double blurRadius() const
Returns the blur radius for the shadow.
static Q_INVOKABLE QString encodeUnit(Qgis::DistanceUnit unit)
Encodes a distance unit to a string.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored)
Contains geos related utilities and functions.
Definition qgsgeos.h:75
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define BUILTIN_UNREACHABLE
Definition qgis.h:6612
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5958
const char * finder(const char *name)
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30