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