QGIS API Documentation 3.40.0-Bratislava (b56115d8743)
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
99 if ( tmpFormat.background().enabled() )
100 {
101 drawPart( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, Qgis::TextComponent::Background, mode );
102 }
103
104 if ( tmpFormat.buffer().enabled() )
105 {
106 drawPart( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, Qgis::TextComponent::Buffer, mode );
107 }
108
109 drawPart( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, Qgis::TextComponent::Text, mode );
110}
111
112void QgsTextRenderer::drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &_format, bool )
113{
114 QgsTextFormat lFormat = _format;
115 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
116 lFormat.updateDataDefinedProperties( context );
117 lFormat = updateShadowPosition( lFormat );
118
119 // DO NOT USE _format in the following code, always use lFormat!!
120 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, lFormat );
121 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
122 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
123
124 drawDocument( point, lFormat, metrics.document(), metrics, context, alignment, rotation );
125}
126
127void QgsTextRenderer::drawDocument( QPointF point, const QgsTextFormat &format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment alignment, double rotation )
128{
129 if ( format.background().enabled() )
130 {
131 drawPart( point, rotation, alignment, document, metrics, context, format, Qgis::TextComponent::Background, Qgis::TextLayoutMode::Point );
132 }
133
134 if ( format.buffer().enabled() )
135 {
136 drawPart( point, rotation, alignment, document, metrics, context, format, Qgis::TextComponent::Buffer, Qgis::TextLayoutMode::Point );
137 }
138
139 drawPart( point, rotation, alignment, document, metrics, context, format, Qgis::TextComponent::Text, Qgis::TextLayoutMode::Point );
140}
141
142void QgsTextRenderer::drawTextOnLine( const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &_format, double offsetAlongLine, double offsetFromLine )
143{
144 QgsTextFormat lFormat = _format;
145 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
146 lFormat.updateDataDefinedProperties( context );
147 lFormat = updateShadowPosition( lFormat );
148
149 // DO NOT USE _format in the following code, always use lFormat!!
150
151 // todo handle newlines??
152 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( {text}, lFormat );
153
154 drawDocumentOnLine( line, lFormat, document, context, offsetAlongLine, offsetFromLine );
155}
156
157void QgsTextRenderer::drawDocumentOnLine( const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine, double offsetFromLine )
158{
159 QPolygonF labelBaselineCurve = line;
160 if ( !qgsDoubleNear( offsetFromLine, 0 ) )
161 {
162 std::unique_ptr < QgsLineString > ring( QgsLineString::fromQPolygonF( line ) );
163 QgsGeos geos( ring.get() );
164 std::unique_ptr < QgsLineString > offsetCurve( dynamic_cast< QgsLineString * >( geos.offsetCurve( offsetFromLine, 4, Qgis::JoinStyle::Round, 2 ) ) );
165 if ( !offsetCurve )
166 return;
167
168#if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<11
169 if ( offsetFromLine < 0 )
170 {
171 // geos < 3.11 reverses the direction of offset curves with negative distances -- we don't want that!
172 std::unique_ptr < QgsLineString > reversed( offsetCurve->reversed() );
173 if ( !reversed )
174 return;
175
176 offsetCurve = std::move( reversed );
177 }
178#endif
179
180 labelBaselineCurve = offsetCurve->asQPolygonF();
181 }
182
183 const double fontScale = calculateScaleFactorForFormat( context, format );
184
185 const QFont baseFont = format.scaledFont( context, fontScale );
186 const double letterSpacing = baseFont.letterSpacing() / fontScale;
187 const double wordSpacing = baseFont.wordSpacing() / fontScale;
188
189 QStringList graphemes;
190 QVector< QgsTextCharacterFormat > graphemeFormats;
191 QVector< QgsTextDocumentMetrics > graphemeMetrics;
192
193 for ( const QgsTextBlock &block : std::as_const( document ) )
194 {
195 for ( const QgsTextFragment &fragment : block )
196 {
197 const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
198 for ( const QString &grapheme : fragmentGraphemes )
199 {
200 graphemes.append( grapheme );
201 graphemeFormats.append( fragment.characterFormat() );
202
203 QgsTextDocument document;
204 document.append( QgsTextBlock( QgsTextFragment( grapheme, fragment.characterFormat() ) ) );
205
206 graphemeMetrics.append( QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale ) );
207 }
208 }
209 }
210
211 QVector< double > characterWidths( graphemes.count() );
212 QVector< double > characterHeights( graphemes.count() );
213 QVector< double > characterDescents( graphemes.count() );
214 QFont previousNonSuperSubScriptFont;
215
216 for ( int i = 0; i < graphemes.count(); i++ )
217 {
218 // reconstruct how Qt creates word spacing, then adjust per individual stored character
219 // this will allow the text renderer to create each candidate width = character width + correct spacing
220
221 double graphemeFirstCharHorizontalAdvanceWithLetterSpacing = 0;
222 double graphemeFirstCharHorizontalAdvance = 0;
223 double graphemeHorizontalAdvance = 0;
224 double characterDescent = 0;
225 double characterHeight = 0;
226 const QgsTextCharacterFormat *graphemeFormat = &graphemeFormats[i];
227
228 QFont graphemeFont = baseFont;
229 graphemeFormat->updateFontForFormat( graphemeFont, context, fontScale );
230
231 if ( i == 0 )
232 previousNonSuperSubScriptFont = graphemeFont;
233
234 if ( graphemeFormat->hasVerticalAlignmentSet() )
235 {
236 switch ( graphemeFormat->verticalAlignment() )
237 {
239 previousNonSuperSubScriptFont = graphemeFont;
240 break;
241
244 {
245 if ( graphemeFormat->fontPointSize() < 0 )
246 {
247 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
248 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
249 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
250 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
251 // which we should respect
252 graphemeFont.setPixelSize( static_cast< int >( std::round( graphemeFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
253 }
254 break;
255 }
256 }
257 }
258 else
259 {
260 previousNonSuperSubScriptFont = graphemeFont;
261 }
262
263 const QFontMetricsF graphemeFontMetrics( graphemeFont );
264 graphemeFirstCharHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ) / fontScale;
265 graphemeFirstCharHorizontalAdvanceWithLetterSpacing = graphemeFontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) / fontScale + letterSpacing;
266 graphemeHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i] ) ) / fontScale;
267 characterDescent = graphemeFontMetrics.descent() / fontScale;
268 characterHeight = graphemeFontMetrics.height() / fontScale;
269
270 qreal wordSpaceFix = qreal( 0.0 );
271 if ( graphemes[i] == QLatin1String( " " ) )
272 {
273 // word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
274 int nxt = i + 1;
275 wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != QLatin1String( " " ) ) ? wordSpacing : qreal( 0.0 );
276 }
277
278 // this workaround only works for clusters with a single character. Not sure how it should be handled
279 // with multi-character clusters.
280 if ( graphemes[i].length() == 1 &&
281 !qgsDoubleNear( graphemeFirstCharHorizontalAdvance, graphemeFirstCharHorizontalAdvanceWithLetterSpacing ) )
282 {
283 // word spacing applied when it shouldn't be
284 wordSpaceFix -= wordSpacing;
285 }
286
287 const double charWidth = graphemeHorizontalAdvance + wordSpaceFix;
288 characterWidths[i] = charWidth;
289 characterHeights[i] = characterHeight;
290 characterDescents[i] = characterDescent;
291 }
292
293 QgsPrecalculatedTextMetrics metrics( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) );
294 metrics.setGraphemeFormats( graphemeFormats );
295
296 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement = QgsTextRendererUtils::generateCurvedTextPlacement(
297 metrics, labelBaselineCurve, offsetAlongLine,
299 -1, -1,
302 );
303
304 if ( placement->graphemePlacement.empty() )
305 return;
306
307 std::vector< QgsTextRenderer::Component > components;
308 components.reserve( placement->graphemePlacement.size() );
309 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
310 {
311 QgsTextRenderer::Component component;
312 component.origin = QPointF( grapheme.x, grapheme.y );
313 component.rotation = -grapheme.angle;
314
315 QgsTextDocumentMetrics &metrics = graphemeMetrics[ grapheme.graphemeIndex ];
316 const double verticalOffset = metrics.fragmentVerticalOffset( 0, 0, Qgis::TextLayoutMode::Point );
317 if ( !qgsDoubleNear( verticalOffset, 0 ) )
318 {
319 component.origin.rx() += verticalOffset * std::cos( grapheme.angle + M_PI_2 );
320 component.origin.ry() += verticalOffset * std::sin( grapheme.angle + M_PI_2 );
321 }
322
323 components.emplace_back( component );
324 }
325
326 if ( format.background().enabled() )
327 {
328 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
329 {
330 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
331 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
332 drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Point );
333 }
334 }
335
336 if ( format.buffer().enabled() )
337 {
338 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
339 {
340 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
341 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
342
343 drawTextInternal( Qgis::TextComponent::Buffer,
344 context,
345 format,
346 component,
347 metrics.document(),
348 metrics,
352 }
353 }
354
355 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
356 {
357 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
358 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
359
360 drawTextInternal( Qgis::TextComponent::Text,
361 context,
362 format,
363 component,
364 metrics.document(),
365 metrics,
369 }
370}
371
372QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format )
373{
375 return format;
376
377 QgsTextFormat tmpFormat = format;
378 if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds
379 {
381 }
382 else if ( tmpFormat.buffer().enabled() )
383 {
385 }
386 else
387 {
389 }
390 return tmpFormat;
391}
392
393void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment,
394 const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
395{
396 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
397 const double fontScale = calculateScaleFactorForFormat( context, format );
398 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
399
400 drawPart( rect, rotation, alignment, Qgis::TextVerticalAlignment::Top, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Rectangle );
401}
402
403void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, Qgis::TextVerticalAlignment vAlignment, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, Qgis::TextLayoutMode mode )
404{
405 if ( !context.painter() )
406 {
407 return;
408 }
409
410 Component component;
411 component.dpiRatio = 1.0;
412 component.origin = rect.topLeft();
413 component.rotation = rotation;
414 component.size = rect.size();
415 component.hAlign = alignment;
416
417 switch ( part )
418 {
420 {
421 if ( !format.background().enabled() )
422 return;
423
424 if ( !qgsDoubleNear( rotation, 0.0 ) )
425 {
426 // get rotated label's center point
427
428 double xc = rect.width() / 2.0;
429 double yc = rect.height() / 2.0;
430
431 double angle = -rotation;
432 double xd = xc * std::cos( angle ) - yc * std::sin( angle );
433 double yd = xc * std::sin( angle ) + yc * std::cos( angle );
434
435 component.center = QPointF( component.origin.x() + xd, component.origin.y() + yd );
436 }
437 else
438 {
439 component.center = rect.center();
440 }
441
442 switch ( vAlignment )
443 {
445 break;
447 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
448 break;
450 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
451 break;
452 }
453
454 QgsTextRenderer::drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Rectangle );
455
456 break;
457 }
458
460 {
461 if ( !format.buffer().enabled() )
462 break;
463 }
464 [[fallthrough]];
467 {
468 drawTextInternal( part, context, format, component,
469 document, metrics,
470 alignment, vAlignment, mode );
471 break;
472 }
473 }
474}
475
476void QgsTextRenderer::drawPart( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
477{
478 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
479 const double fontScale = calculateScaleFactorForFormat( context, format );
480 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
481
482 drawPart( origin, rotation, alignment, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Point );
483}
484
485void QgsTextRenderer::drawPart( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, Qgis::TextLayoutMode mode )
486{
487 if ( !context.painter() )
488 {
489 return;
490 }
491
492 Component component;
493 component.dpiRatio = 1.0;
494 component.origin = origin;
495 component.rotation = rotation;
496 component.hAlign = alignment;
497
498 switch ( part )
499 {
501 {
502 if ( !format.background().enabled() )
503 return;
504
505 QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
506 break;
507 }
508
510 {
511 if ( !format.buffer().enabled() )
512 break;
513 }
514 [[fallthrough]];
517 {
518 drawTextInternal( part, context, format, component,
519 document,
520 metrics,
522 mode );
523 break;
524 }
525 }
526}
527
528QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
529{
530 return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
531}
532
533double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format,
534 const QgsTextDocumentMetrics &metrics,
536{
537 QPainter *p = context.painter();
538
539 Qgis::TextOrientation orientation = format.orientation();
541 {
542 if ( component.rotation >= -315 && component.rotation < -90 )
543 {
545 }
546 else if ( component.rotation >= -90 && component.rotation < -45 )
547 {
549 }
550 else
551 {
553 }
554 }
555
556 QgsTextBufferSettings buffer = format.buffer();
557
558 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage
559 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
560 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
561
562 const double scaleFactor = calculateScaleFactorForFormat( context, format );
563
564 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
565 if ( mode == Qgis::TextLayoutMode::Labeling )
566 {
567 // label size has already been calculated using any symbology reference scale factor -- we need
568 // to temporarily remove the reference scale here or we'll be applying the scaling twice
569 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
570 }
571
572 if ( metrics.isNullFontSize() )
573 return 0;
574
575 referenceScaleOverride.reset();
576
577 QPainterPath path;
578 path.setFillRule( Qt::WindingFill );
579 double advance = 0;
580 double height = component.size.height();
581 switch ( orientation )
582 {
584 {
585 double xOffset = 0;
586 int fragmentIndex = 0;
587 for ( const QgsTextFragment &fragment : component.block )
588 {
589 QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
590
591 if ( !fragment.isWhitespace() && !fragment.isImage() )
592 {
593 if ( component.extraWordSpacing || component.extraLetterSpacing )
594 applyExtraSpacingForLineJustification( fragmentFont, component.extraWordSpacing, component.extraLetterSpacing );
595
596 const double yOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
597 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
598 }
599
600 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
601
602 fragmentIndex++;
603 }
604 advance = xOffset;
605 break;
606 }
607
610 {
611 double partYOffset = component.offset.y() * scaleFactor;
612
613 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
614 double partLastDescent = 0;
615
616 int fragmentIndex = 0;
617 for ( const QgsTextFragment &fragment : component.block )
618 {
619 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
620 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
621
622 const QFontMetricsF fragmentMetrics( fragmentFont );
623
624 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
625
626 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
627 for ( const QString &part : parts )
628 {
629 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
630 partYOffset += fragmentMetrics.ascent() / scaleFactor;
631 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
632 partYOffset += letterSpacing;
633 }
634 partLastDescent = fragmentMetrics.descent() / scaleFactor;
635
636 fragmentIndex++;
637 }
638 height = partYOffset + partLastDescent;
639 advance = partYOffset - component.offset.y() * scaleFactor;
640 break;
641 }
642 }
643
644 QColor bufferColor = buffer.color();
645 bufferColor.setAlphaF( buffer.opacity() );
646 QPen pen( bufferColor );
647 pen.setWidthF( penSize * scaleFactor );
648 pen.setJoinStyle( buffer.joinStyle() );
649 QColor tmpColor( bufferColor );
650 // honor pref for whether to fill buffer interior
651 if ( !buffer.fillBufferInterior() )
652 {
653 tmpColor.setAlpha( 0 );
654 }
655
656 // store buffer's drawing in QPicture for drop shadow call
657 QPicture buffPict;
658 QPainter buffp;
659 buffp.begin( &buffPict );
660 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
661 {
662 context.setPainter( &buffp );
663 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
664
665 tmpEffect->begin( context );
666 context.painter()->setPen( pen );
667 context.painter()->setBrush( tmpColor );
668 if ( scaleFactor != 1.0 )
669 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
670 context.painter()->drawPath( path );
671 if ( scaleFactor != 1.0 )
672 context.painter()->scale( scaleFactor, scaleFactor );
673 tmpEffect->end( context );
674
675 context.setPainter( p );
676 }
677 else
678 {
679 if ( scaleFactor != 1.0 )
680 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
681 buffp.setPen( pen );
682 buffp.setBrush( tmpColor );
683 buffp.drawPath( path );
684 }
685 buffp.end();
686
688 {
689 QgsTextRenderer::Component bufferComponent = component;
690 bufferComponent.origin = QPointF( 0.0, 0.0 );
691 bufferComponent.picture = buffPict;
692 bufferComponent.pictureBuffer = penSize / 2.0;
693 bufferComponent.size.setHeight( height );
694
696 {
697 bufferComponent.offset.setY( - bufferComponent.size.height() );
698 }
699 drawShadow( context, bufferComponent, format );
700 }
701
702 QgsScopedQPainterState painterState( p );
703 context.setPainterFlagsUsingContext( p );
704
705 if ( context.useAdvancedEffects() )
706 {
707 p->setCompositionMode( buffer.blendMode() );
708 }
709
710 // scale for any print output or image saving @ specific dpi
711 p->scale( component.dpiRatio, component.dpiRatio );
713 p->drawPicture( 0, 0, buffPict );
714
715 return advance / scaleFactor;
716}
717
718void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics,
720{
721 QgsTextMaskSettings mask = format.mask();
722
723 // the mask is drawn to a side painter
724 // or to the main painter for preview
725 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
726 if ( ! p )
727 return;
728
729 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage
730 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
731 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
732
733 // buffer: draw the text with a big pen
734 QPainterPath path;
735 path.setFillRule( Qt::WindingFill );
736
737 const double scaleFactor = calculateScaleFactorForFormat( context, format );
738
739 // TODO: vertical text mode was ignored when masking feature was added.
740 // Hopefully Oslandia come back and fix this? Hint hint...
741
742 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
743 if ( mode == Qgis::TextLayoutMode::Labeling )
744 {
745 // label size has already been calculated using any symbology reference scale factor -- we need
746 // to temporarily remove the reference scale here or we'll be applying the scaling twice
747 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
748 }
749
750 if ( metrics.isNullFontSize() )
751 return;
752
753 referenceScaleOverride.reset();
754
755 double xOffset = 0;
756 int fragmentIndex = 0;
757 for ( const QgsTextFragment &fragment : component.block )
758 {
759 if ( !fragment.isWhitespace() && !fragment.isImage() )
760 {
761 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
762
763 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
764 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
765 }
766
767 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
768 fragmentIndex++;
769 }
770
771 QColor bufferColor( Qt::gray );
772 bufferColor.setAlphaF( mask.opacity() );
773
774 QPen pen;
775 QBrush brush;
776 brush.setColor( bufferColor );
777 pen.setColor( bufferColor );
778 pen.setWidthF( penSize * scaleFactor );
779 pen.setJoinStyle( mask.joinStyle() );
780
781 QgsScopedQPainterState painterState( p );
782 context.setPainterFlagsUsingContext( p );
783
784 // scale for any print output or image saving @ specific dpi
785 p->scale( component.dpiRatio, component.dpiRatio );
786 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
787 {
788 QgsPainterSwapper swapper( context, p );
789 {
790 QgsEffectPainter effectPainter( context, mask.paintEffect() );
791 if ( scaleFactor != 1.0 )
792 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
793 context.painter()->setPen( pen );
794 context.painter()->setBrush( brush );
795 context.painter()->drawPath( path );
796 if ( scaleFactor != 1.0 )
797 context.painter()->scale( scaleFactor, scaleFactor );
798 }
799 }
800 else
801 {
802 if ( scaleFactor != 1.0 )
803 p->scale( 1 / scaleFactor, 1 / scaleFactor );
804 p->setPen( pen );
805 p->setBrush( brush );
806 p->drawPath( path );
807 if ( scaleFactor != 1.0 )
808 p->scale( scaleFactor, scaleFactor );
809
810 }
811}
812
813double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
814{
815 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( textLines, format );
816 if ( doc.size() == 0 )
817 return 0;
818
819 return textWidth( context, format, doc );
820}
821
822double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
823{
824 //calculate max width of text lines
825 const double scaleFactor = calculateScaleFactorForFormat( context, format );
826
827 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
828
829 // width doesn't change depending on layout mode, we can use anything here
830 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
831}
832
833double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth )
834{
835 QStringList lines;
836 for ( const QString &line : textLines )
837 {
838 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
839 {
840 lines.append( wrappedText( context, line, maxLineWidth, format ) );
841 }
842 else
843 {
844 lines.append( line );
845 }
846 }
847
848 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( lines, format );
849 return textHeight( context, format, doc, mode );
850}
851
852double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
853{
854 const double scaleFactor = calculateScaleFactorForFormat( context, format );
855
856 bool isNullSize = false;
857 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
858 if ( isNullSize )
859 return 0;
860
861 const QFontMetrics fm( baseFont );
862 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
863
864 if ( !includeEffects )
865 return height;
866
867 double maxExtension = 0;
868 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
869 if ( format.buffer().enabled() )
870 {
871 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
872 ? fontSize * format.buffer().size() / 100
873 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
874 }
875 if ( format.shadow().enabled() )
876 {
877 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
878 ? fontSize * format.shadow().offsetDistance() / 100
879 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
880 )
882 ? fontSize * format.shadow().blurRadius() / 100
883 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
884 );
885 }
886 if ( format.background().enabled() )
887 {
888 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
890 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
891 {
892 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
893 }
894 }
895
896 return height + maxExtension;
897}
898
899bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
900{
901 if ( qgsDoubleNear( width, 0.0 ) )
902 return false;
903
904 const QStringList multiLineSplit = text.split( '\n' );
905 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
906 return currentTextWidth > width;
907}
908
909QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
910{
911 const QStringList lines = text.split( '\n' );
912 QStringList outLines;
913 for ( const QString &line : lines )
914 {
915 if ( textRequiresWrapping( context, line, width, format ) )
916 {
917 //first step is to identify words which must be on their own line (too long to fit)
918 const QStringList words = line.split( ' ' );
919 QStringList linesToProcess;
920 QString wordsInCurrentLine;
921 for ( const QString &word : words )
922 {
923 if ( textRequiresWrapping( context, word, width, format ) )
924 {
925 //too long to fit
926 if ( !wordsInCurrentLine.isEmpty() )
927 linesToProcess << wordsInCurrentLine;
928 wordsInCurrentLine.clear();
929 linesToProcess << word;
930 }
931 else
932 {
933 if ( !wordsInCurrentLine.isEmpty() )
934 wordsInCurrentLine.append( ' ' );
935 wordsInCurrentLine.append( word );
936 }
937 }
938 if ( !wordsInCurrentLine.isEmpty() )
939 linesToProcess << wordsInCurrentLine;
940
941 for ( const QString &line : std::as_const( linesToProcess ) )
942 {
943 QString remainingText = line;
944 int lastPos = remainingText.lastIndexOf( ' ' );
945 while ( lastPos > -1 )
946 {
947 //check if remaining text is short enough to go in one line
948 if ( !textRequiresWrapping( context, remainingText, width, format ) )
949 {
950 break;
951 }
952
953 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
954 {
955 outLines << remainingText.left( lastPos );
956 remainingText = remainingText.mid( lastPos + 1 );
957 lastPos = 0;
958 }
959 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
960 }
961 outLines << remainingText;
962 }
963 }
964 else
965 {
966 outLines << line;
967 }
968 }
969
970 return outLines;
971}
972
973double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
974{
975 QgsTextDocument document = doc;
976 document.applyCapitalization( format.capitalization() );
977
978 //calculate max height of text lines
979 const double scaleFactor = calculateScaleFactorForFormat( context, format );
980
981 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
982 if ( metrics.isNullFontSize() )
983 return 0;
984
985 return metrics.documentSize( mode, format.orientation() ).height();
986}
987
988void QgsTextRenderer::drawBackground( QgsRenderContext &context, QgsTextRenderer::Component component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
989{
990 QgsTextBackgroundSettings background = format.background();
991
992 QPainter *prevP = context.painter();
993 QPainter *p = context.painter();
994 std::unique_ptr< QgsPaintEffect > tmpEffect;
995 if ( background.paintEffect() && background.paintEffect()->enabled() )
996 {
997 tmpEffect.reset( background.paintEffect()->clone() );
998 tmpEffect->begin( context );
999 p = context.painter();
1000 }
1001
1002 //QgsDebugMsgLevel( QStringLiteral( "Background label rotation: %1" ).arg( component.rotation() ), 4 );
1003
1004 // shared calculations between shapes and SVG
1005
1006 // configure angles, set component rotation and rotationOffset
1007 const double originAdjustRotationRadians = -component.rotation;
1009 {
1010 component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
1011 component.rotationOffset =
1012 background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
1013 }
1014 else // RotationFixed
1015 {
1016 component.rotation = 0.0; // don't use label's rotation
1017 component.rotationOffset = background.rotation();
1018 }
1019
1020 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1021
1022 if ( mode != Qgis::TextLayoutMode::Labeling )
1023 {
1024 // need to calculate size of text
1025 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1026 double width = documentSize.width();
1027 double height = documentSize.height();
1028
1029 switch ( mode )
1030 {
1034 switch ( component.hAlign )
1035 {
1038 component.center = QPointF( component.origin.x() + width / 2.0,
1039 component.origin.y() + height / 2.0 );
1040 break;
1041
1043 component.center = QPointF( component.origin.x() + component.size.width() / 2.0,
1044 component.origin.y() + height / 2.0 );
1045 break;
1046
1048 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0,
1049 component.origin.y() + height / 2.0 );
1050 break;
1051 }
1052 break;
1053
1055 {
1056 bool isNullSize = false;
1057 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1058 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1059 switch ( component.hAlign )
1060 {
1063 component.center = QPointF( component.origin.x() + width / 2.0,
1064 component.origin.y() - height / 2.0 + originAdjust );
1065 break;
1066
1068 component.center = QPointF( component.origin.x(),
1069 component.origin.y() - height / 2.0 + originAdjust );
1070 break;
1071
1073 component.center = QPointF( component.origin.x() - width / 2.0,
1074 component.origin.y() - height / 2.0 + originAdjust );
1075 break;
1076 }
1077
1078 // apply rotation to center point
1079 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1080 {
1081 const double dx = component.center.x() - component.origin.x();
1082 const double dy = component.center.y() - component.origin.y();
1083 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1084 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1085 }
1086 break;
1087 }
1088
1090 break;
1091 }
1092
1094 component.size = QSizeF( width, height );
1095 }
1096
1097 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1098
1099 switch ( background.type() )
1100 {
1103 {
1104 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1105
1106 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1107 return;
1108
1109 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1110 return;
1111
1112 double sizeOut = 0.0;
1113 {
1114 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1115
1116 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1117 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1118 {
1119 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1120 }
1121 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1122 {
1123 sizeOut = std::max( component.size.width(), component.size.height() );
1124 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1125
1126 // add buffer
1127 sizeOut += bufferSize * 2;
1128 }
1129 }
1130
1131 // don't bother rendering symbols smaller than 1x1 pixels in size
1132 // TODO: add option to not show any svgs under/over a certain size
1133 if ( sizeOut < 1.0 )
1134 return;
1135
1136 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1137 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1138 {
1139 QVariantMap map; // for SVG symbology marker
1140 map[QStringLiteral( "name" )] = background.svgFile().trimmed();
1141 map[QStringLiteral( "size" )] = QString::number( sizeOut );
1142 map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1143 map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter
1144
1145 // offset is handled by this local painter
1146 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1147 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1148 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1149 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1150
1151 map[QStringLiteral( "fill" )] = background.fillColor().name();
1152 map[QStringLiteral( "outline" )] = background.strokeColor().name();
1153 map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() );
1154 map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1155
1157 {
1158 QgsTextShadowSettings shadow = format.shadow();
1159 // configure SVG shadow specs
1160 QVariantMap shdwmap( map );
1161 shdwmap[QStringLiteral( "fill" )] = shadow.color().name();
1162 shdwmap[QStringLiteral( "outline" )] = shadow.color().name();
1163 shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut );
1164
1165 // store SVG's drawing in QPicture for drop shadow call
1166 QPicture svgPict;
1167 QPainter svgp;
1168 svgp.begin( &svgPict );
1169
1170 // draw shadow symbol
1171
1172 // clone current render context map unit/mm conversion factors, but not
1173 // other map canvas parameters, then substitute this painter for use in symbology painting
1174 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1175 // but will be created relative to the SVG's computed size, not the current map canvas
1176 QgsRenderContext shdwContext;
1177 shdwContext.setMapToPixel( context.mapToPixel() );
1178 shdwContext.setScaleFactor( context.scaleFactor() );
1179 shdwContext.setPainter( &svgp );
1180
1181 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1182 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1183 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1184
1185 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1186 svgp.end();
1187
1188 component.picture = svgPict;
1189 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1190 component.pictureBuffer = 0.0;
1191
1192 component.size = QSizeF( sizeOut, sizeOut );
1193 component.offset = QPointF( 0.0, 0.0 );
1194
1195 // rotate about origin center of SVG
1196 QgsScopedQPainterState painterState( p );
1197 context.setPainterFlagsUsingContext( p );
1198
1199 p->translate( component.center.x(), component.center.y() );
1200 p->rotate( component.rotation );
1201 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1202 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1203 p->translate( QPointF( xoff, yoff ) );
1204 p->rotate( component.rotationOffset );
1205 p->translate( -sizeOut / 2, sizeOut / 2 );
1206
1207 drawShadow( context, component, format );
1208 }
1209 renderedSymbol.reset( );
1210
1212 renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) );
1213 }
1214 else
1215 {
1216 renderedSymbol.reset( background.markerSymbol()->clone() );
1217 renderedSymbol->setSize( sizeOut );
1218 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1219 }
1220
1221 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1222
1223 // draw the actual symbol
1224 QgsScopedQPainterState painterState( p );
1225 context.setPainterFlagsUsingContext( p );
1226
1227 if ( context.useAdvancedEffects() )
1228 {
1229 p->setCompositionMode( background.blendMode() );
1230 }
1231 p->translate( component.center.x(), component.center.y() );
1232 p->rotate( component.rotation );
1233 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1234 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1235 p->translate( QPointF( xoff, yoff ) );
1236 p->rotate( component.rotationOffset );
1237
1238 const QgsFeature f = context.expressionContext().feature();
1239 renderedSymbol->startRender( context, context.expressionContext().fields() );
1240 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1241 renderedSymbol->stopRender( context );
1242 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1243
1244 break;
1245 }
1246
1251 {
1252 double w = component.size.width();
1253 double h = component.size.height();
1254
1255 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1256 {
1257 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1258 background.sizeMapUnitScale() );
1259 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1260 background.sizeMapUnitScale() );
1261 }
1262 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1263 {
1264 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1265 {
1266 if ( w > h )
1267 h = w;
1268 else if ( h > w )
1269 w = h;
1270 }
1271 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1272 {
1273 // start with label bound by circle
1274 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1275 w = h;
1276 }
1277 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1278 {
1279 // start with label bound by ellipse
1280 h = h * M_SQRT1_2 * 2;
1281 w = w * M_SQRT1_2 * 2;
1282 }
1283
1284 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1285 background.sizeMapUnitScale() );
1286 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1287 background.sizeMapUnitScale() );
1288
1289 w += bufferWidth * 2;
1290 h += bufferHeight * 2;
1291 }
1292
1293 // offsets match those of symbology: -x = left, -y = up
1294 QRectF rect( -w / 2.0, - h / 2.0, w, h );
1295
1296 if ( rect.isNull() )
1297 return;
1298
1299 QgsScopedQPainterState painterState( p );
1300 context.setPainterFlagsUsingContext( p );
1301
1302 p->translate( QPointF( component.center.x(), component.center.y() ) );
1303 p->rotate( component.rotation );
1304 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1305 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1306 p->translate( QPointF( xoff, yoff ) );
1307 p->rotate( component.rotationOffset );
1308
1309 QPainterPath path;
1310
1311 // Paths with curves must be enlarged before conversion to QPolygonF, or
1312 // the curves are approximated too much and appear jaggy
1313 QTransform t = QTransform::fromScale( 10, 10 );
1314 // inverse transform used to scale created polygons back to expected size
1315 QTransform ti = t.inverted();
1316
1318 || background.type() == QgsTextBackgroundSettings::ShapeSquare )
1319 {
1320 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1321 {
1322 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1323 }
1324 else
1325 {
1326 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1327 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1328 path.addRoundedRect( rect, xRadius, yRadius );
1329 }
1330 }
1331 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse
1332 || background.type() == QgsTextBackgroundSettings::ShapeCircle )
1333 {
1334 path.addEllipse( rect );
1335 }
1336 QPolygonF tempPolygon = path.toFillPolygon( t );
1337 QPolygonF polygon = ti.map( tempPolygon );
1338 QPicture shapePict;
1339 QPainter *oldp = context.painter();
1340 QPainter shapep;
1341
1342 shapep.begin( &shapePict );
1343 context.setPainter( &shapep );
1344
1345 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1346 renderedSymbol.reset( background.fillSymbol()->clone() );
1347 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1348
1349 const QgsFeature f = context.expressionContext().feature();
1350 renderedSymbol->startRender( context, context.expressionContext().fields() );
1351 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1352 renderedSymbol->stopRender( context );
1353
1354 shapep.end();
1355 context.setPainter( oldp );
1356
1358 {
1359 component.picture = shapePict;
1360 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1361
1362 component.size = rect.size();
1363 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1364 drawShadow( context, component, format );
1365 }
1366
1367 if ( context.useAdvancedEffects() )
1368 {
1369 p->setCompositionMode( background.blendMode() );
1370 }
1371
1372 // scale for any print output or image saving @ specific dpi
1373 p->scale( component.dpiRatio, component.dpiRatio );
1375 p->drawPicture( 0, 0, shapePict );
1376 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1377 break;
1378 }
1379 }
1380
1381 if ( tmpEffect )
1382 {
1383 tmpEffect->end( context );
1384 context.setPainter( prevP );
1385 }
1386}
1387
1388void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1389{
1390 QgsTextShadowSettings shadow = format.shadow();
1391
1392 // incoming component sizes should be multiplied by rasterCompressFactor, as
1393 // this allows shadows to be created at paint device dpi (e.g. high resolution),
1394 // then scale device painter by 1.0 / rasterCompressFactor for output
1395
1396 QPainter *p = context.painter();
1397 const double componentWidth = component.size.width();
1398 const double componentHeight = component.size.height();
1399 double xOffset = component.offset.x(), yOffset = component.offset.y();
1400 double pictbuffer = component.pictureBuffer;
1401
1402 // generate pixmap representation of label component drawing
1403 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1404
1405 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1406 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage
1407 ? fontSize * shadow.blurRadius() / 100
1408 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1409 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1410 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1411
1412 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1413 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1414 double blurBufferClippingScale = 3.75;
1415 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1416
1417 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1418 componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1419 QImage::Format_ARGB32_Premultiplied );
1420
1421 // TODO: add labeling gui option to not show any shadows under/over a certain size
1422 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1423 int minBlurImgSize = 1;
1424 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1425 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1426 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1427 int maxBlurImgSize = 40000;
1428 if ( blurImg.isNull()
1429 || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize )
1430 || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1431 return;
1432
1433 blurImg.fill( QColor( Qt::transparent ).rgba() );
1434 QPainter pictp;
1435 if ( !pictp.begin( &blurImg ) )
1436 return;
1437 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1438 QPointF imgOffset( blurbuffer + pictbuffer + xOffset,
1439 blurbuffer + pictbuffer + componentHeight + yOffset );
1440
1441 pictp.drawPicture( imgOffset,
1442 component.picture );
1443
1444 // overlay shadow color
1445 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1446 pictp.fillRect( blurImg.rect(), shadow.color() );
1447 pictp.end();
1448
1449 // blur the QImage in-place
1450 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1451 {
1452 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1453 }
1454
1455#if 0
1456 // debug rect for QImage shadow registration and clipping visualization
1457 QPainter picti;
1458 picti.begin( &blurImg );
1459 picti.setBrush( Qt::Dense7Pattern );
1460 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1461 imgPen.setWidth( 1 );
1462 picti.setPen( imgPen );
1463 picti.setOpacity( 0.1 );
1464 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1465 picti.end();
1466#endif
1467
1468 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage
1469 ? fontSize * shadow.offsetDistance() / 100
1470 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1471 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1472 if ( shadow.offsetGlobal() )
1473 {
1474 // TODO: check for differences in rotation origin and cw/ccw direction,
1475 // when this shadow function is used for something other than labels
1476
1477 // it's 0-->cw-->360 for labels
1478 //QgsDebugMsgLevel( QStringLiteral( "Shadow aggregated label rotation (degrees): %1" ).arg( component.rotation() + component.rotationOffset() ), 4 );
1479 angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1480 }
1481
1482 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ),
1483 -offsetDist * std::sin( angleRad + M_PI_2 ) );
1484
1485 p->save();
1486 context.setPainterFlagsUsingContext( p );
1487 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1488 // corresponding flag in the render context instead...
1489 p->setRenderHint( QPainter::SmoothPixmapTransform );
1490 if ( context.useAdvancedEffects() )
1491 {
1492 p->setCompositionMode( shadow.blendMode() );
1493 }
1494 p->setOpacity( shadow.opacity() );
1495
1496 double scale = shadow.scale() / 100.0;
1497 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1498 p->scale( scale, scale );
1499 if ( component.useOrigin )
1500 {
1501 p->translate( component.origin.x(), component.origin.y() );
1502 }
1503 p->translate( transPt );
1504 p->translate( -imgOffset.x(),
1505 -imgOffset.y() );
1506 p->drawImage( 0, 0, blurImg );
1507 p->restore();
1508
1509 // debug rects
1510#if 0
1511 // draw debug rect for QImage painting registration
1512 p->save();
1513 p->setBrush( Qt::NoBrush );
1514 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1515 imgPen.setWidth( 2 );
1516 imgPen.setStyle( Qt::DashLine );
1517 p->setPen( imgPen );
1518 p->scale( scale, scale );
1519 if ( component.useOrigin() )
1520 {
1521 p->translate( component.origin().x(), component.origin().y() );
1522 }
1523 p->translate( transPt );
1524 p->translate( -imgOffset.x(),
1525 -imgOffset.y() );
1526 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1527 p->restore();
1528
1529 // draw debug rect for passed in component dimensions
1530 p->save();
1531 p->setBrush( Qt::NoBrush );
1532 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1533 componentRectPen.setWidth( 1 );
1534 if ( component.useOrigin() )
1535 {
1536 p->translate( component.origin().x(), component.origin().y() );
1537 }
1538 p->setPen( componentRectPen );
1539 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1540 p->restore();
1541#endif
1542}
1543
1544
1545void QgsTextRenderer::drawTextInternal( Qgis::TextComponent drawType,
1546 QgsRenderContext &context,
1547 const QgsTextFormat &format,
1548 const Component &component,
1549 const QgsTextDocument &document,
1550 const QgsTextDocumentMetrics &metrics,
1552{
1553 if ( !context.painter() )
1554 {
1555 return;
1556 }
1557
1558 const double fontScale = calculateScaleFactorForFormat( context, format );
1559
1560 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1561 if ( mode == Qgis::TextLayoutMode::Labeling )
1562 {
1563 // label size has already been calculated using any symbology reference scale factor -- we need
1564 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1565 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1566 }
1567
1568 if ( metrics.isNullFontSize() )
1569 return;
1570
1571 referenceScaleOverride.reset();
1572
1573 double rotation = 0;
1574 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1575 switch ( orientation )
1576 {
1578 {
1579 drawTextInternalHorizontal( context, format, drawType, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1580 break;
1581 }
1582
1585 {
1586 drawTextInternalVertical( context, format, drawType, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1587 break;
1588 }
1589 }
1590}
1591
1592Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1593{
1594 rotation = -component.rotation * 180 / M_PI;
1595
1596 switch ( format.orientation() )
1597 {
1599 {
1600 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1601 if ( rotation >= -315 && rotation < -90 )
1602 {
1603 rotation -= 90;
1605 }
1606 else if ( rotation >= -90 && rotation < -45 )
1607 {
1608 rotation += 90;
1610 }
1611
1613 }
1614
1617 return format.orientation();
1618 }
1620}
1621
1622void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1623{
1624 const QString blockText = block.toPlainText();
1625 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1626 finder.toStart();
1627 int wordBoundaries = 0;
1628 while ( finder.toNextBoundary() != -1 )
1629 {
1630 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1631 wordBoundaries++;
1632 }
1633
1634 if ( wordBoundaries > 0 )
1635 {
1636 // word boundaries found => justify by padding word spacing
1637 extraWordSpace = spaceToDistribute / wordBoundaries;
1638 }
1639 else
1640 {
1641 // no word boundaries found => justify by letter spacing
1642 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1643 finder.toStart();
1644
1645 int graphemeBoundaries = 0;
1646 while ( finder.toNextBoundary() != -1 )
1647 {
1648 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1649 graphemeBoundaries++;
1650 }
1651
1652 if ( graphemeBoundaries > 0 )
1653 {
1654 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1655 }
1656 }
1657}
1658
1659void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1660{
1661 const double prevWordSpace = font.wordSpacing();
1662 font.setWordSpacing( prevWordSpace + extraWordSpace );
1663 const double prevLetterSpace = font.letterSpacing();
1664 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1665}
1666
1667
1668void QgsTextRenderer::renderBlockHorizontal( const QgsTextBlock &block, int blockIndex,
1669 const QgsTextDocumentMetrics &metrics, QgsRenderContext &context,
1670 const QgsTextFormat &format,
1671 QPainter *painter, bool usePaths,
1672 double fontScale, double extraWordSpace, double extraLetterSpace,
1674{
1675 if ( !metrics.isNullFontSize() )
1676 {
1677 double xOffset = 0;
1678 int fragmentIndex = 0;
1679 for ( const QgsTextFragment &fragment : block )
1680 {
1681 // draw text, QPainterPath method
1682 if ( !fragment.isWhitespace() && !fragment.isImage() )
1683 {
1684 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1685
1686 if ( !qgsDoubleNear( extraWordSpace, 0 ) || !qgsDoubleNear( extraLetterSpace, 0 ) )
1687 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1688
1689 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1690
1691 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1692 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1693
1694 if ( usePaths )
1695 {
1696 painter->setBrush( textColor );
1697 QPainterPath path;
1698 path.setFillRule( Qt::WindingFill );
1699 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1700 painter->drawPath( path );
1701 }
1702 else
1703 {
1704 painter->setPen( textColor );
1705 painter->setFont( fragmentFont );
1706 painter->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1707 }
1708 }
1709 else if ( fragment.isImage() )
1710 {
1711 bool fitsInCache = false;
1712 const double imageWidth = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1713 const double imageHeight = metrics.fragmentFixedHeight( blockIndex, fragmentIndex, mode ) * fontScale;
1714
1715 const QImage image = QgsApplication::imageCache()->pathAsImage( fragment.characterFormat().imagePath(),
1716 QSize( static_cast< int >( std::round( imageWidth ) ),
1717 static_cast< int >( std::round( imageHeight ) ) ),
1718 false,
1719 1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1720 const double imageBaseline = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1721 const double yOffset = imageBaseline - image.height();
1722 if ( !image.isNull() )
1723 painter->drawImage( QPointF( xOffset, yOffset ), image );
1724 }
1725
1726 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1727 fragmentIndex ++;
1728 }
1729 }
1730};
1731
1732bool QgsTextRenderer::usePathsToRender( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
1733{
1734 switch ( context.textRenderFormat() )
1735 {
1737 return true;
1739 return false;
1741 {
1742 // Prefer not to use paths -- but certain conditions will require us to use them
1743 if ( format.buffer().enabled() )
1744 {
1745 // text buffer requires use of paths
1746 // TODO: this was the original cause of use switching from text to paths by default,
1747 // but that was way back in the 2.0 days and maybe the Qt issues have now been fixed?
1748 return true;
1749 }
1750
1751 // underline/overline/strikethrough looks different between path/non-path renders.
1752 // TODO: validate which is correct. For now, maintain default appearance from before this code
1753 // was introduced
1754 if ( format.font().underline()
1755 || format.font().overline()
1756 || format.font().strikeOut()
1757 || std::any_of( document.begin(), document.end(), []( const QgsTextBlock & block )
1758 {
1759 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment & fragment )
1760 {
1761 return fragment.characterFormat().underline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1762 || fragment.characterFormat().overline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1763 || fragment.characterFormat().strikeOut() == QgsTextCharacterFormat::BooleanValue::SetTrue;
1764 } );
1765 } ) )
1766 return true;
1767
1768 return false;
1769 }
1770 }
1772}
1773void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent drawType, Qgis::TextLayoutMode mode, const Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, const Qgis::TextHorizontalAlignment hAlignment,
1774 Qgis::TextVerticalAlignment vAlignment, double rotation )
1775{
1776 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1777 const QStringList textLines = document.toPlainText();
1778
1779 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
1780
1781 double labelWidest = 0.0;
1782 switch ( mode )
1783 {
1786 labelWidest = documentSize.width();
1787 break;
1788
1792 labelWidest = component.size.width();
1793 break;
1794 }
1795
1796 double verticalAlignOffset = 0;
1797
1799 {
1800 const double overallHeight = documentSize.height();
1801 switch ( vAlignment )
1802 {
1804 break;
1805
1807 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5;
1808 break;
1809
1811 verticalAlignOffset = ( component.size.height() - overallHeight );
1812 break;
1813 }
1814 }
1815
1816 // should we use text or paths for this render?
1817 const bool usePaths = usePathsToRender( context, format, document );
1818
1819 int blockIndex = 0;
1820 for ( const QgsTextBlock &block : document )
1821 {
1822 Qgis::TextHorizontalAlignment blockAlignment = hAlignment;
1823 if ( block.blockFormat().hasHorizontalAlignmentSet() )
1824 blockAlignment = block.blockFormat().horizontalAlignment();
1825 const bool adjustForAlignment = blockAlignment != Qgis::TextHorizontalAlignment::Left &&
1827 || textLines.size() > 1 );
1828
1829 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 )
1830 || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1831
1832 const double blockHeight = metrics.blockHeight( blockIndex );
1833
1834 QgsScopedQPainterState painterState( context.painter() );
1836 context.painter()->translate( component.origin );
1837 if ( !qgsDoubleNear( rotation, 0.0 ) )
1838 context.painter()->rotate( rotation );
1839
1840 // apply to the mask painter the same transformations
1841 if ( maskPainter )
1842 {
1843 maskPainter->save();
1844 maskPainter->translate( component.origin );
1845 if ( !qgsDoubleNear( rotation, 0.0 ) )
1846 maskPainter->rotate( rotation );
1847 }
1848
1849 // figure x offset for horizontal alignment of multiple lines
1850 double xMultiLineOffset = 0.0;
1851 double blockWidth = metrics.blockWidth( blockIndex );
1852 double extraWordSpace = 0;
1853 double extraLetterSpace = 0;
1854 if ( adjustForAlignment )
1855 {
1856 double labelWidthDiff = 0;
1857 switch ( blockAlignment )
1858 {
1860 labelWidthDiff = ( labelWidest - blockWidth ) * 0.5;
1861 break;
1862
1864 labelWidthDiff = labelWidest - blockWidth;
1865 break;
1866
1868 if ( !isFinalLineInParagraph && labelWidest > blockWidth )
1869 {
1870 calculateExtraSpacingForLineJustification( labelWidest - blockWidth, block, extraWordSpace, extraLetterSpace );
1871 blockWidth = labelWidest;
1872 }
1873 break;
1874
1876 break;
1877 }
1878
1879 switch ( mode )
1880 {
1885 xMultiLineOffset = labelWidthDiff;
1886 break;
1887
1889 {
1890 switch ( blockAlignment )
1891 {
1893 xMultiLineOffset = labelWidthDiff - labelWidest;
1894 break;
1895
1897 xMultiLineOffset = labelWidthDiff - labelWidest / 2.0;
1898 break;
1899
1902 break;
1903 }
1904 }
1905 break;
1906 }
1907 }
1908
1909 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1910
1911 context.painter()->translate( QPointF( xMultiLineOffset, baseLineOffset + verticalAlignOffset ) );
1912 if ( maskPainter )
1913 maskPainter->translate( QPointF( xMultiLineOffset, baseLineOffset + verticalAlignOffset ) );
1914
1915 Component subComponent;
1916 subComponent.block = block;
1917 subComponent.blockIndex = blockIndex;
1918 subComponent.size = QSizeF( blockWidth, blockHeight );
1919 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
1920 subComponent.rotation = -component.rotation * 180 / M_PI;
1921 subComponent.rotationOffset = 0.0;
1922 subComponent.extraWordSpacing = extraWordSpace * fontScale;
1923 subComponent.extraLetterSpacing = extraLetterSpace * fontScale;
1924
1925 // draw the mask below the text (for preview)
1926 if ( format.mask().enabled() )
1927 {
1928 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
1929 }
1930
1931 if ( drawType == Qgis::TextComponent::Buffer )
1932 {
1933 QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
1934 }
1935 else
1936 {
1937 // store text's drawing in QPicture for drop shadow call
1938
1939 const bool drawShadowOnText = format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText;
1940 // do we need to store text temporarily in a QPicture? Avoid if we can...
1941 const bool requiresPicture = drawShadowOnText;
1942
1943 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1944 if ( mode == Qgis::TextLayoutMode::Labeling )
1945 {
1946 // label size has already been calculated using any symbology reference scale factor -- we need
1947 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1948 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1949 }
1950
1951 referenceScaleOverride.reset();
1952
1953 std::unique_ptr< QPicture > textPicture;
1954 if ( requiresPicture )
1955 {
1956 // render to picture
1957 textPicture = std::make_unique< QPicture >();
1958 QPainter picturePainter( textPicture.get() );
1959 picturePainter.setPen( Qt::NoPen );
1960 picturePainter.setBrush( Qt::NoBrush );
1961 picturePainter.scale( 1 / fontScale, 1 / fontScale );
1962 renderBlockHorizontal( block, blockIndex, metrics, context, format, &picturePainter, usePaths,
1963 fontScale, extraWordSpace, extraLetterSpace, mode );
1964 picturePainter.end();
1965 }
1966
1967 if ( drawShadowOnText )
1968 {
1969 subComponent.picture = *textPicture;
1970 subComponent.pictureBuffer = 0.0; // no pen width to deal with
1971 subComponent.origin = QPointF( 0.0, 0.0 );
1972
1973 QgsTextRenderer::drawShadow( context, subComponent, format );
1974 }
1975
1976 // now render the actual text
1977 if ( context.useAdvancedEffects() )
1978 {
1979 context.painter()->setCompositionMode( format.blendMode() );
1980 }
1981
1982 // scale for any print output or image saving @ specific dpi
1983 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
1984
1985 if ( textPicture )
1986 {
1988 context.painter()->drawPicture( 0, 0, *textPicture );
1989 }
1990 else
1991 {
1992 context.painter()->scale( 1 / fontScale, 1 / fontScale );
1993 context.painter()->setPen( Qt::NoPen );
1994 context.painter()->setBrush( Qt::NoBrush );
1995 renderBlockHorizontal( block, blockIndex, metrics, context, format, context.painter(), usePaths,
1996 fontScale, extraWordSpace, extraLetterSpace, mode );
1997 }
1998 }
1999 if ( maskPainter )
2000 maskPainter->restore();
2001
2002 blockIndex++;
2003 }
2004}
2005
2006void QgsTextRenderer::drawTextInternalVertical( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent drawType, Qgis::TextLayoutMode mode, const QgsTextRenderer::Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, Qgis::TextHorizontalAlignment hAlignment, Qgis::TextVerticalAlignment, double rotation )
2007{
2008 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2009 const QStringList textLines = document.toPlainText();
2010
2011 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2012 if ( mode == Qgis::TextLayoutMode::Labeling )
2013 {
2014 // label size has already been calculated using any symbology reference scale factor -- we need
2015 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2016 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2017 }
2018
2019 if ( metrics.isNullFontSize() )
2020 return;
2021
2022 referenceScaleOverride.reset();
2023
2024 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
2025 const double actualTextWidth = documentSize.width();
2026 double textRectWidth = 0.0;
2027
2028 switch ( mode )
2029 {
2032 textRectWidth = actualTextWidth;
2033 break;
2034
2038 textRectWidth = component.size.width();
2039 break;
2040 }
2041
2042 int maxLineLength = 0;
2043 for ( const QString &line : std::as_const( textLines ) )
2044 {
2045 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2046 }
2047
2048 const double actualLabelHeight = documentSize.height();
2049 int blockIndex = 0;
2050
2051 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2052
2053 for ( const QgsTextBlock &block : document )
2054 {
2055 QgsScopedQPainterState painterState( context.painter() );
2057
2058 context.painter()->translate( component.origin );
2059 if ( !qgsDoubleNear( rotation, 0.0 ) )
2060 context.painter()->rotate( rotation );
2061
2062 // apply to the mask painter the same transformations
2063 if ( maskPainter )
2064 {
2065 maskPainter->save();
2066 maskPainter->translate( component.origin );
2067 if ( !qgsDoubleNear( rotation, 0.0 ) )
2068 maskPainter->rotate( rotation );
2069 }
2070
2071 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2072
2073 // figure x offset of multiple lines
2074 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2075 if ( adjustForAlignment )
2076 {
2077 double hAlignmentOffset = 0;
2078 switch ( hAlignment )
2079 {
2081 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2082 break;
2083
2085 hAlignmentOffset = textRectWidth - actualTextWidth;
2086 break;
2087
2090 break;
2091 }
2092
2093 switch ( mode )
2094 {
2099 xOffset += hAlignmentOffset;
2100 break;
2101
2103 break;
2104 }
2105 }
2106
2107 double yOffset = 0.0;
2108 switch ( mode )
2109 {
2112 {
2113 if ( rotation >= -405 && rotation < -180 )
2114 {
2115 yOffset = 0;
2116 }
2117 else if ( rotation >= 0 && rotation < 45 )
2118 {
2119 xOffset -= actualTextWidth;
2120 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2121 }
2122 }
2123 else
2124 {
2125 yOffset = -actualLabelHeight;
2126 }
2127 break;
2128
2130 yOffset = -actualLabelHeight;
2131 break;
2132
2136 yOffset = 0;
2137 break;
2138 }
2139
2140 context.painter()->translate( QPointF( xOffset, yOffset ) );
2141
2142 double currentBlockYOffset = 0;
2143 int fragmentIndex = 0;
2144 for ( const QgsTextFragment &fragment : block )
2145 {
2146 QgsScopedQPainterState fragmentPainterState( context.painter() );
2147
2148 // apply some character replacement to draw symbols in vertical presentation
2149 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2150
2151 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2152
2153 QFontMetricsF fragmentMetrics( fragmentFont );
2154
2155 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2156 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2157
2158 Component subComponent;
2159 subComponent.block = QgsTextBlock( fragment );
2160 subComponent.blockIndex = blockIndex;
2161 subComponent.firstFragmentIndex = fragmentIndex;
2162 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2163 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2164 subComponent.rotation = -component.rotation * 180 / M_PI;
2165 subComponent.rotationOffset = 0.0;
2166
2167 // draw the mask below the text (for preview)
2168 if ( format.mask().enabled() )
2169 {
2170 // WARNING: totally broken! (has been since mask was introduced)
2171#if 0
2172 QgsTextRenderer::drawMask( context, subComponent, format );
2173#endif
2174 }
2175
2176 if ( drawType == Qgis::TextComponent::Buffer )
2177 {
2178 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2179 }
2180 else
2181 {
2182 // draw text, QPainterPath method
2183 QPainterPath path;
2184 path.setFillRule( Qt::WindingFill );
2185 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2186 double partYOffset = 0.0;
2187 for ( const QString &part : parts )
2188 {
2189 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2190 partYOffset += fragmentMetrics.ascent() / fontScale;
2191 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2192 partYOffset += letterSpacing;
2193 }
2194
2195 // store text's drawing in QPicture for drop shadow call
2196 QPicture textPict;
2197 QPainter textp;
2198 textp.begin( &textPict );
2199 textp.setPen( Qt::NoPen );
2200 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2201 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2202 textp.setBrush( textColor );
2203 textp.scale( 1 / fontScale, 1 / fontScale );
2204 textp.drawPath( path );
2205
2206 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2207 // e.g. some capitalization options, but not others
2208 //textp.setFont( tmpLyr.textFont );
2209 //textp.setPen( tmpLyr.textColor );
2210 //textp.drawText( 0, 0, component.text() );
2211 textp.end();
2212
2213 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2214 {
2215 subComponent.picture = textPict;
2216 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2217 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2218 const double prevY = subComponent.offset.y();
2219 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2220 subComponent.useOrigin = true;
2221 QgsTextRenderer::drawShadow( context, subComponent, format );
2222 subComponent.useOrigin = false;
2223 subComponent.offset = QPointF( 0, prevY );
2224 }
2225
2226 // paint the text
2227 if ( context.useAdvancedEffects() )
2228 {
2229 context.painter()->setCompositionMode( format.blendMode() );
2230 }
2231
2232 // scale for any print output or image saving @ specific dpi
2233 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2234
2235 // TODO -- this should respect the context's TextRenderFormat
2236 // draw outlined text
2237 context.painter()->translate( 0, currentBlockYOffset );
2239 context.painter()->drawPicture( 0, 0, textPict );
2240 currentBlockYOffset += partYOffset;
2241 }
2242 fragmentIndex++;
2243 }
2244
2245 if ( maskPainter )
2246 maskPainter->restore();
2247 blockIndex++;
2248 }
2249}
2250
2252{
2254 return 1.0;
2255
2256 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2257
2258 // THESE THRESHOLDS MAY NEED TWEAKING!
2259
2260 // NOLINTBEGIN(bugprone-branch-clone)
2261
2262 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2263 // 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
2264 if ( pixelSize < 50 )
2265 return 200 / pixelSize;
2266 //... 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!
2267 // 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
2268 else if ( pixelSize > 200 )
2269 return 200 / pixelSize;
2270 else
2271 return 1.0;
2272
2273 // NOLINTEND(bugprone-branch-clone)
2274}
2275
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:3154
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:4839
@ 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:2751
@ Bottom
Align to bottom.
@ VerticalCenter
Center align.
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:2732
@ 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
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 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 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.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
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 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.
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.
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:6571
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5917
const char * finder(const char *name)
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30