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