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