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