QGIS API Documentation 4.3.0-Master (0978c174f8e)
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
18#include <memory>
19#include <optional>
20
21#include "qgsapplication.h"
22#include "qgsfillsymbol.h"
23#include "qgsgeos.h"
24#include "qgsimagecache.h"
25#include "qgsmarkersymbol.h"
27#include "qgspainteffect.h"
28#include "qgspainterswapper.h"
29#include "qgspainting.h"
30#include "qgspallabeling.h"
31#include "qgssymbollayerutils.h"
32#include "qgstextdocument.h"
34#include "qgstextformat.h"
35#include "qgstextfragment.h"
36#include "qgstextmetrics.h"
38#include "qgsunittypes.h"
39
40#include <QString>
41#include <QTextBoundaryFinder>
42
43using namespace Qt::StringLiterals;
44
46{
47 if ( alignment & Qt::AlignLeft )
49 else if ( alignment & Qt::AlignRight )
51 else if ( alignment & Qt::AlignHCenter )
53 else if ( alignment & Qt::AlignJustify )
55
56 // not supported?
58}
59
61{
62 if ( alignment & Qt::AlignTop )
64 else if ( alignment & Qt::AlignBottom )
66 else if ( alignment & Qt::AlignVCenter )
68 //not supported
69 else if ( alignment & Qt::AlignBaseline )
71
73}
74
75int QgsTextRenderer::sizeToPixel( double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale )
76{
77 return static_cast< int >( c.convertToPainterUnits( size, unit, mapUnitScale ) + 0.5 ); //NOLINT
78}
79
81 const QRectF &rect,
82 double rotation,
84 const QStringList &text,
85 QgsRenderContext &context,
86 const QgsTextFormat &_format,
87 bool,
91)
92{
93 QgsTextFormat lFormat = _format;
94 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use format instead of tmpFormat here, it's const and potentially avoids a detach
95 lFormat.updateDataDefinedProperties( context );
96
97 // DO NOT USE _format in the following code, always use lFormat!!
98 QgsTextDocumentRenderContext documentContext;
99 documentContext.setFlags( flags );
100 documentContext.setMaximumWidth( rect.width() );
101
102 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( text, lFormat );
103
104 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
105 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale, documentContext );
106
107 drawDocument( rect, lFormat, metrics.document(), metrics, context, alignment, vAlignment, rotation, mode, flags );
108}
109
111 const QRectF &rect,
112 const QgsTextFormat &format,
113 const QgsTextDocument &document,
114 const QgsTextDocumentMetrics &metrics,
115 QgsRenderContext &context,
116 Qgis::TextHorizontalAlignment horizontalAlignment,
117 Qgis::TextVerticalAlignment verticalAlignment,
118 double rotation,
121)
122{
123 const QgsTextFormat tmpFormat = updateShadowPosition( format );
124
126 if ( tmpFormat.background().enabled() )
127 {
129 }
130
131 if ( tmpFormat.shadow().enabled() )
132 {
133 components |= Qgis::TextComponent::Shadow;
134 }
135
136 if ( tmpFormat.buffer().enabled() )
137 {
138 components |= Qgis::TextComponent::Buffer;
139 }
140
141 drawParts( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, components, mode );
142}
143
144void QgsTextRenderer::drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &_format, bool )
145{
146 QgsTextFormat lFormat = _format;
147 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
148 lFormat.updateDataDefinedProperties( context );
149 lFormat = updateShadowPosition( lFormat );
150
151 // DO NOT USE _format in the following code, always use lFormat!!
152 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, lFormat );
153 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
154 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
155
156 drawDocument( point, lFormat, metrics.document(), metrics, context, alignment, rotation );
157}
158
160 QPointF point,
161 const QgsTextFormat &_format,
162 const QgsTextDocument &document,
163 const QgsTextDocumentMetrics &metrics,
164 QgsRenderContext &context,
166 double rotation,
168)
169{
170 const QgsTextFormat lFormat = updateShadowPosition( _format );
171 // DO NOT USE _format in the following code, always use lFormat!!
172
174 if ( lFormat.background().enabled() )
175 {
177 }
178
179 if ( lFormat.shadow().enabled() )
180 {
181 components |= Qgis::TextComponent::Shadow;
182 }
183
184 if ( lFormat.buffer().enabled() )
185 {
186 components |= Qgis::TextComponent::Buffer;
187 }
188
189 drawParts( point, rotation, alignment, document, metrics, context, lFormat, components, mode );
190}
191
193 const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &_format, double offsetAlongLine, double offsetFromLine, Qgis::CurvedTextFlags flags, Qgis::TextAnchorPoint textAnchor
194)
195{
196 QgsTextFormat lFormat = _format;
197 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
198 lFormat.updateDataDefinedProperties( context );
199 lFormat = updateShadowPosition( lFormat );
200
201 // DO NOT USE _format in the following code, always use lFormat!!
202
203 // todo handle newlines??
204 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( { text }, lFormat );
205
206 drawDocumentOnLine( line, lFormat, document, context, offsetAlongLine, offsetFromLine, flags, textAnchor );
207}
208
210 const QPolygonF &line,
211 const QgsTextFormat &format,
212 const QgsTextDocument &document,
213 QgsRenderContext &context,
214 double offsetAlongLine,
215 double offsetFromLine,
217 Qgis::TextAnchorPoint textAnchor
218)
219{
220 QPolygonF labelBaselineCurve = line;
221 if ( !qgsDoubleNear( offsetFromLine, 0 ) )
222 {
223 std::unique_ptr< QgsLineString > ring( QgsLineString::fromQPolygonF( line ) );
224 QgsGeos geos( ring.get() );
225 std::unique_ptr< QgsLineString > offsetCurve( dynamic_cast< QgsLineString * >( geos.offsetCurve( offsetFromLine, 4, Qgis::JoinStyle::Round, 2 ) ) );
226 if ( !offsetCurve )
227 return;
228
229#if GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 11
230 if ( offsetFromLine < 0 )
231 {
232 // geos < 3.11 reverses the direction of offset curves with negative distances -- we don't want that!
233 std::unique_ptr< QgsLineString > reversed( offsetCurve->reversed() );
234 if ( !reversed )
235 return;
236
237 offsetCurve = std::move( reversed );
238 }
239#endif
240
241 labelBaselineCurve = offsetCurve->asQPolygonF();
242 }
243
244 const double fontScale = calculateScaleFactorForFormat( context, format );
245
246 const QFont baseFont = format.scaledFont( context, fontScale );
247 const double letterSpacing = baseFont.letterSpacing() / fontScale;
248 const double wordSpacing = baseFont.wordSpacing() / fontScale;
249
250 QStringList graphemes;
251 QVector< QgsTextCharacterFormat > graphemeFormats;
252 QVector< QgsTextDocumentMetrics > graphemeMetrics;
253
254 for ( const QgsTextBlock &block : std::as_const( document ) )
255 {
256 for ( const QgsTextFragment &fragment : block )
257 {
258 const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
259 for ( const QString &grapheme : fragmentGraphemes )
260 {
261 graphemes.append( grapheme );
262 graphemeFormats.append( fragment.characterFormat() );
263
264 QgsTextDocument document;
265 document.append( QgsTextBlock( QgsTextFragment( grapheme, fragment.characterFormat() ) ) );
266
267 graphemeMetrics.append( QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale ) );
268 }
269 }
270 }
271
272 QVector< double > characterWidths( graphemes.count() );
273 QVector< double > characterHeights( graphemes.count() );
274 QVector< double > characterDescents( graphemes.count() );
275 QFont previousNonSuperSubScriptFont;
276
277 for ( int i = 0; i < graphemes.count(); i++ )
278 {
279 // reconstruct how Qt creates word spacing, then adjust per individual stored character
280 // this will allow the text renderer to create each candidate width = character width + correct spacing
281
282 double graphemeFirstCharHorizontalAdvanceWithLetterSpacing = 0;
283 double graphemeFirstCharHorizontalAdvance = 0;
284 double graphemeHorizontalAdvance = 0;
285 double characterDescent = 0;
286 double characterHeight = 0;
287 const QgsTextCharacterFormat *graphemeFormat = &graphemeFormats[i];
288
289 QFont graphemeFont = baseFont;
290 graphemeFormat->updateFontForFormat( graphemeFont, context, fontScale );
291
292 if ( i == 0 )
293 previousNonSuperSubScriptFont = graphemeFont;
294
295 if ( graphemeFormat->hasVerticalAlignmentSet() )
296 {
297 switch ( graphemeFormat->verticalAlignment() )
298 {
300 previousNonSuperSubScriptFont = graphemeFont;
301 break;
302
305 {
306 if ( graphemeFormat->fontPointSize() < 0 )
307 {
308 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
309 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
310 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
311 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
312 // which we should respect
313 graphemeFont.setPixelSize( static_cast< int >( std::round( graphemeFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
314 }
315 break;
316 }
317 }
318 }
319 else
320 {
321 previousNonSuperSubScriptFont = graphemeFont;
322 }
323
324 const QFontMetricsF graphemeFontMetrics( graphemeFont );
325 graphemeFirstCharHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ) / fontScale;
326 graphemeFirstCharHorizontalAdvanceWithLetterSpacing = graphemeFontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) / fontScale + letterSpacing;
327 graphemeHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i] ) ) / fontScale;
328 characterDescent = graphemeFontMetrics.descent() / fontScale;
329 characterHeight = graphemeFontMetrics.height() / fontScale;
330
331 qreal wordSpaceFix = qreal( 0.0 );
332 if ( graphemes[i] == " "_L1 )
333 {
334 // word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
335 int nxt = i + 1;
336 wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != " "_L1 ) ? wordSpacing : qreal( 0.0 );
337 }
338
339 // this workaround only works for clusters with a single character. Not sure how it should be handled
340 // with multi-character clusters.
341 if ( graphemes[i].length() == 1 && !qgsDoubleNear( graphemeFirstCharHorizontalAdvance, graphemeFirstCharHorizontalAdvanceWithLetterSpacing ) )
342 {
343 // word spacing applied when it shouldn't be
344 wordSpaceFix -= wordSpacing;
345 }
346
347 const double charWidth = graphemeHorizontalAdvance + wordSpaceFix;
348 characterWidths[i] = charWidth;
349 characterHeights[i] = characterHeight;
350 characterDescents[i] = characterDescent;
351 }
352
353 QgsPrecalculatedTextMetrics metrics( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) );
354 metrics.setGraphemeFormats( graphemeFormats );
355
356 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement
357 = QgsTextRendererUtils::generateCurvedTextPlacement( metrics, labelBaselineCurve, offsetAlongLine, QgsTextRendererUtils::RespectPainterOrientation, -1, -1, flags, textAnchor );
358
359 if ( placement->graphemePlacement.empty() )
360 return;
361
362 // We may have deliberately skipped over some graphemes during curved text placement (such as zero-width graphemes).
363 // So we need to use a hash of the original grapheme index to place generated components in, as there may accordingly
364 // be graphemes which don't result in components, and we can't just blindly assume the component array position
365 // will match the original grapheme index
366 QHash< int, QgsTextRenderer::Component > components;
367 components.reserve( placement->graphemePlacement.size() );
368 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
369 {
370 QgsTextRenderer::Component component;
371 component.origin = QPointF( grapheme.x, grapheme.y );
372 component.rotation = -grapheme.angle;
373
374 QgsTextDocumentMetrics &metrics = graphemeMetrics[grapheme.graphemeIndex];
375 const double verticalOffset = metrics.fragmentVerticalOffset( 0, 0, Qgis::TextLayoutMode::Point );
376 if ( !qgsDoubleNear( verticalOffset, 0 ) )
377 {
378 component.origin.rx() += verticalOffset * std::cos( grapheme.angle + M_PI_2 );
379 component.origin.ry() += verticalOffset * std::sin( grapheme.angle + M_PI_2 );
380 }
381
382 components.insert( grapheme.graphemeIndex, component );
383 }
384
385 if ( format.background().enabled() )
386 {
387 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
388 {
389 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
390 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex];
391 drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Point );
392 }
393 }
394
395 if ( format.buffer().enabled() )
396 {
397 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
398 {
399 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
400 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex];
401
403 }
404 }
405
406 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
407 {
408 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
409 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex];
410
412 }
413}
414
415QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format )
416{
418 return format;
419
420 QgsTextFormat tmpFormat = format;
421 if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds
422 {
424 }
425 else if ( tmpFormat.buffer().enabled() )
426 {
428 }
429 else
430 {
432 }
433 return tmpFormat;
434}
435
437 const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool
438)
439{
440 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
441 const double fontScale = calculateScaleFactorForFormat( context, format );
442 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
443
444 drawParts( rect, rotation, alignment, Qgis::TextVerticalAlignment::Top, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Rectangle );
445}
446
447void QgsTextRenderer::drawParts(
448 const QRectF &rect,
449 double rotation,
452 const QgsTextDocument &document,
453 const QgsTextDocumentMetrics &metrics,
454 QgsRenderContext &context,
455 const QgsTextFormat &format,
458)
459{
460 if ( !context.painter() )
461 {
462 return;
463 }
464
465 Component component;
466 component.dpiRatio = 1.0;
467 component.origin = rect.topLeft();
468 component.rotation = rotation;
469 component.size = rect.size();
470 component.hAlign = alignment;
471
472 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
473 {
474 if ( !qgsDoubleNear( rotation, 0.0 ) )
475 {
476 // get rotated label's center point
477
478 double xc = rect.width() / 2.0;
479 double yc = rect.height() / 2.0;
480
481 double angle = -rotation;
482 double xd = xc * std::cos( angle ) - yc * std::sin( angle );
483 double yd = xc * std::sin( angle ) + yc * std::cos( angle );
484
485 component.center = QPointF( component.origin.x() + xd, component.origin.y() + yd );
486 }
487 else
488 {
489 component.center = rect.center();
490 }
491
492 switch ( vAlignment )
493 {
495 break;
497 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
498 break;
500 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
501 break;
502 }
503
504 QgsTextRenderer::drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Rectangle );
505 }
506
507 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
508 {
509 return;
510 }
511
513 {
514 drawTextInternal( parts, context, format, component, document, metrics, alignment, vAlignment, mode );
515 }
516}
517
519 QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool
520)
521{
522 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
523 const double fontScale = calculateScaleFactorForFormat( context, format );
524 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
525
526 drawParts( origin, rotation, alignment, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Point );
527}
528
529void QgsTextRenderer::drawParts(
530 QPointF origin,
531 double rotation,
533 const QgsTextDocument &document,
534 const QgsTextDocumentMetrics &metrics,
535 QgsRenderContext &context,
536 const QgsTextFormat &format,
539)
540{
541 if ( !context.painter() )
542 {
543 return;
544 }
545
546 Component component;
547 component.dpiRatio = 1.0;
548 component.origin = origin;
549 component.rotation = rotation;
550 component.hAlign = alignment;
551
552 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
553 {
554 QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
555 }
556
557 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
558 {
559 return;
560 }
561
563 {
564 drawTextInternal( parts, context, format, component, document, metrics, alignment, Qgis::TextVerticalAlignment::Top, mode );
565 }
566}
567
568QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
569{
570 return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
571}
572
573double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
574{
575 QPainter *p = context.painter();
576
577 Qgis::TextOrientation orientation = format.orientation();
579 {
580 if ( component.rotation >= -315 && component.rotation < -90 )
581 {
583 }
584 else if ( component.rotation >= -90 && component.rotation < -45 )
585 {
587 }
588 else
589 {
591 }
592 }
593
594 QgsTextBufferSettings buffer = format.buffer();
595
596 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
597 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
598
599 const double scaleFactor = calculateScaleFactorForFormat( context, format );
600
601 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
602 if ( mode == Qgis::TextLayoutMode::Labeling )
603 {
604 // label size has already been calculated using any symbology reference scale factor -- we need
605 // to temporarily remove the reference scale here or we'll be applying the scaling twice
606 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
607 }
608
609 if ( metrics.isNullFontSize() )
610 return 0;
611
612 referenceScaleOverride.reset();
613
614 QPainterPath path;
615 path.setFillRule( Qt::WindingFill );
616 double advance = 0;
617 double height = component.size.height();
618 switch ( orientation )
619 {
621 {
622 // NOT SUPPORTED BY THIS METHOD ANYMORE -- buffer drawing is handled in drawTextInternalHorizontal since QGIS 3.42
623 break;
624 }
625
628 {
629 double partYOffset = component.offset.y() * scaleFactor;
630
631 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
632 double partLastDescent = 0;
633
634 int fragmentIndex = 0;
635 for ( const QgsTextFragment &fragment : component.block )
636 {
637 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
638 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
639
640 const QFontMetricsF fragmentMetrics( fragmentFont );
641
642 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
643
644 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
645 for ( const QString &part : parts )
646 {
647 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
648 partYOffset += fragmentMetrics.ascent() / scaleFactor;
649 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
650 partYOffset += letterSpacing;
651 }
652 partLastDescent = fragmentMetrics.descent() / scaleFactor;
653
654 fragmentIndex++;
655 }
656 height = partYOffset + partLastDescent;
657 advance = partYOffset - component.offset.y() * scaleFactor;
658 break;
659 }
660 }
661
662 QColor bufferColor = buffer.color();
663 bufferColor.setAlphaF( buffer.opacity() );
664 QPen pen( bufferColor );
665 pen.setWidthF( penSize * scaleFactor );
666 pen.setJoinStyle( buffer.joinStyle() );
667 QColor tmpColor( bufferColor );
668 // honor pref for whether to fill buffer interior
669 if ( !buffer.fillBufferInterior() )
670 {
671 tmpColor.setAlpha( 0 );
672 }
673
674 // store buffer's drawing in QPicture for drop shadow call
675 QPicture buffPict;
676 QPainter buffp;
677 buffp.begin( &buffPict );
678 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
679 {
680 context.setPainter( &buffp );
681 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
682
683 tmpEffect->begin( context );
684 context.painter()->setPen( pen );
685 context.painter()->setBrush( tmpColor );
686 if ( scaleFactor != 1.0 )
687 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
688 context.painter()->drawPath( path );
689 if ( scaleFactor != 1.0 )
690 context.painter()->scale( scaleFactor, scaleFactor );
691 tmpEffect->end( context );
692
693 context.setPainter( p );
694 }
695 else
696 {
697 if ( scaleFactor != 1.0 )
698 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
699 buffp.setPen( pen );
700 buffp.setBrush( tmpColor );
701 buffp.drawPath( path );
702 }
703 buffp.end();
704
706 {
707 QgsTextRenderer::Component bufferComponent = component;
708 bufferComponent.origin = QPointF( 0.0, 0.0 );
709 bufferComponent.picture = buffPict;
710 bufferComponent.pictureBuffer = penSize / 2.0;
711 bufferComponent.size.setHeight( height );
712
714 {
715 bufferComponent.offset.setY( -bufferComponent.size.height() );
716 }
717 drawShadow( context, bufferComponent, format );
718 }
719
720 QgsScopedQPainterState painterState( p );
721 context.setPainterFlagsUsingContext( p );
722
724 {
725 p->setCompositionMode( buffer.blendMode() );
726 }
727
728 // scale for any print output or image saving @ specific dpi
729 p->scale( component.dpiRatio, component.dpiRatio );
731 p->drawPicture( 0, 0, buffPict );
732
733 return advance / scaleFactor;
734}
735
736void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
737{
738 QgsTextMaskSettings mask = format.mask();
739
740 // the mask is drawn to a side painter
741 // or to the main painter for preview
742 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
743 if ( !p )
744 return;
745
746 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
747 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
748
749 // buffer: draw the text with a big pen
750 QPainterPath path;
751 path.setFillRule( Qt::WindingFill );
752
753 const double scaleFactor = calculateScaleFactorForFormat( context, format );
754
755 // TODO: vertical text mode was ignored when masking feature was added.
756 // Hopefully Oslandia come back and fix this? Hint hint...
757
758 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
759 if ( mode == Qgis::TextLayoutMode::Labeling )
760 {
761 // label size has already been calculated using any symbology reference scale factor -- we need
762 // to temporarily remove the reference scale here or we'll be applying the scaling twice
763 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
764 }
765
766 if ( metrics.isNullFontSize() )
767 return;
768
769 referenceScaleOverride.reset();
770
771 double xOffset = 0;
772 int fragmentIndex = 0;
773 for ( const QgsTextFragment &fragment : component.block )
774 {
775 if ( !fragment.isWhitespace() && !fragment.isImage() )
776 {
777 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
778
779 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
780 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
781 }
782
783 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
784 fragmentIndex++;
785 }
786
787 QColor bufferColor( Qt::gray );
788 bufferColor.setAlphaF( mask.opacity() );
789
790 QPen pen;
791 QBrush brush;
792 brush.setColor( bufferColor );
793 pen.setColor( bufferColor );
794 pen.setWidthF( penSize * scaleFactor );
795 pen.setJoinStyle( mask.joinStyle() );
796
797 QgsScopedQPainterState painterState( p );
798 context.setPainterFlagsUsingContext( p );
799
800 // scale for any print output or image saving @ specific dpi
801 p->scale( component.dpiRatio, component.dpiRatio );
802 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
803 {
804 QgsPainterSwapper swapper( context, p );
805 {
806 QgsEffectPainter effectPainter( context, mask.paintEffect() );
807 if ( scaleFactor != 1.0 )
808 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
809 context.painter()->setPen( pen );
810 context.painter()->setBrush( brush );
811 context.painter()->drawPath( path );
812 if ( scaleFactor != 1.0 )
813 context.painter()->scale( scaleFactor, scaleFactor );
814 }
815 }
816 else
817 {
818 if ( scaleFactor != 1.0 )
819 p->scale( 1 / scaleFactor, 1 / scaleFactor );
820 p->setPen( pen );
821 p->setBrush( brush );
822 p->drawPath( path );
823 if ( scaleFactor != 1.0 )
824 p->scale( scaleFactor, scaleFactor );
825 }
826}
827
828double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
829{
830 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( textLines, format );
831 if ( doc.size() == 0 )
832 return 0;
833
834 return textWidth( context, format, doc );
835}
836
837double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
838{
839 //calculate max width of text lines
840 const double scaleFactor = calculateScaleFactorForFormat( context, format );
841
842 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
843
844 // width doesn't change depending on layout mode, we can use anything here
845 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
846}
847
849 const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth
850)
851{
852 QStringList lines;
853 for ( const QString &line : textLines )
854 {
855 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
856 {
857 lines.append( wrappedText( context, line, maxLineWidth, format ) );
858 }
859 else
860 {
861 lines.append( line );
862 }
863 }
864
865 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( lines, format );
866 return textHeight( context, format, doc, mode );
867}
868
869double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
870{
871 const double scaleFactor = calculateScaleFactorForFormat( context, format );
872
873 bool isNullSize = false;
874 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
875 if ( isNullSize )
876 return 0;
877
878 const QFontMetrics fm( baseFont );
879 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
880
881 if ( !includeEffects )
882 return height;
883
884 double maxExtension = 0;
885 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
886 if ( format.buffer().enabled() )
887 {
888 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage ? fontSize * format.buffer().size() / 100
889 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
890 }
891 if ( format.shadow().enabled() )
892 {
893 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
894 ? fontSize * format.shadow().offsetDistance() / 100
895 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
896 )
897 + ( format.shadow().blurRadiusUnit() == Qgis::RenderUnit::Percentage
898 ? fontSize * format.shadow().blurRadius() / 100
899 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
900 );
901 }
902 if ( format.background().enabled() )
903 {
904 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
906 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
907 {
908 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
909 }
910 }
911
912 return height + maxExtension;
913}
914
915bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
916{
917 if ( qgsDoubleNear( width, 0.0 ) )
918 return false;
919
920 const QStringList multiLineSplit = text.split( '\n' );
921 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
922 return currentTextWidth > width;
923}
924
925QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
926{
927 const QStringList lines = text.split( '\n' );
928 QStringList outLines;
929 for ( const QString &line : lines )
930 {
931 if ( textRequiresWrapping( context, line, width, format ) )
932 {
933 //first step is to identify words which must be on their own line (too long to fit)
934 const QStringList words = line.split( ' ' );
935 QStringList linesToProcess;
936 QString wordsInCurrentLine;
937 for ( const QString &word : words )
938 {
939 if ( textRequiresWrapping( context, word, width, format ) )
940 {
941 //too long to fit
942 if ( !wordsInCurrentLine.isEmpty() )
943 linesToProcess << wordsInCurrentLine;
944 wordsInCurrentLine.clear();
945 linesToProcess << word;
946 }
947 else
948 {
949 if ( !wordsInCurrentLine.isEmpty() )
950 wordsInCurrentLine.append( ' ' );
951 wordsInCurrentLine.append( word );
952 }
953 }
954 if ( !wordsInCurrentLine.isEmpty() )
955 linesToProcess << wordsInCurrentLine;
956
957 for ( const QString &line : std::as_const( linesToProcess ) )
958 {
959 QString remainingText = line;
960 int lastPos = remainingText.lastIndexOf( ' ' );
961 while ( lastPos > -1 )
962 {
963 //check if remaining text is short enough to go in one line
964 if ( !textRequiresWrapping( context, remainingText, width, format ) )
965 {
966 break;
967 }
968
969 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
970 {
971 outLines << remainingText.left( lastPos );
972 remainingText = remainingText.mid( lastPos + 1 );
973 lastPos = 0;
974 }
975 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
976 }
977 outLines << remainingText;
978 }
979 }
980 else
981 {
982 outLines << line;
983 }
984 }
985
986 return outLines;
987}
988
989double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
990{
991 QgsTextDocument document = doc;
992 document.applyCapitalization( format.capitalization() );
993
994 //calculate max height of text lines
995 const double scaleFactor = calculateScaleFactorForFormat( context, format );
996
997 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
998 if ( metrics.isNullFontSize() )
999 return 0;
1000
1001 return metrics.documentSize( mode, format.orientation() ).height();
1002}
1003
1004void QgsTextRenderer::drawBackground( QgsRenderContext &context, const QgsTextRenderer::Component &c, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
1005{
1006 Component component = c;
1007 QgsTextBackgroundSettings background = format.background();
1008
1009 QPainter *prevP = context.painter();
1010 QPainter *p = context.painter();
1011 std::unique_ptr< QgsPaintEffect > tmpEffect;
1012 if ( background.paintEffect() && background.paintEffect()->enabled() )
1013 {
1014 tmpEffect.reset( background.paintEffect()->clone() );
1015 tmpEffect->begin( context );
1016 p = context.painter();
1017 }
1018
1019 //QgsDebugMsgLevel( u"Background label rotation: %1"_s.arg( component.rotation() ), 4 );
1020
1021 // shared calculations between shapes and SVG
1022
1023 // configure angles, set component rotation and rotationOffset
1024 const double originAdjustRotationRadians = -component.rotation;
1026 {
1027 component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
1028 component.rotationOffset = background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
1029 }
1030 else // RotationFixed
1031 {
1032 component.rotation = 0.0; // don't use label's rotation
1033 component.rotationOffset = background.rotation();
1034 }
1035
1036 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1037
1038 if ( mode != Qgis::TextLayoutMode::Labeling )
1039 {
1040 // need to calculate size of text
1041 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1042 double width = documentSize.width();
1043 double height = documentSize.height();
1044
1045 switch ( mode )
1046 {
1050 switch ( component.hAlign )
1051 {
1054 component.center = QPointF( component.origin.x() + width / 2.0, component.origin.y() + height / 2.0 );
1055 break;
1056
1058 component.center = QPointF( component.origin.x() + component.size.width() / 2.0, component.origin.y() + height / 2.0 );
1059 break;
1060
1062 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0, component.origin.y() + height / 2.0 );
1063 break;
1064 }
1065 break;
1066
1068 {
1069 bool isNullSize = false;
1070 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1071 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1072 switch ( component.hAlign )
1073 {
1076 component.center = QPointF( component.origin.x() + width / 2.0, component.origin.y() - height / 2.0 + originAdjust );
1077 break;
1078
1080 component.center = QPointF( component.origin.x(), component.origin.y() - height / 2.0 + originAdjust );
1081 break;
1082
1084 component.center = QPointF( component.origin.x() - width / 2.0, component.origin.y() - height / 2.0 + originAdjust );
1085 break;
1086 }
1087
1088 // apply rotation to center point
1089 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1090 {
1091 const double dx = component.center.x() - component.origin.x();
1092 const double dy = component.center.y() - component.origin.y();
1093 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1094 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1095 }
1096 break;
1097 }
1098
1100 break;
1101 }
1102
1104 component.size = QSizeF( width, height );
1105 }
1106
1107 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1108
1109 switch ( background.type() )
1110 {
1113 {
1114 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1115
1116 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1117 return;
1118
1119 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1120 return;
1121
1122 double sizeOut = 0.0;
1123 {
1124 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1125
1126 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1127 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1128 {
1129 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1130 }
1131 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1132 {
1133 sizeOut = std::max( component.size.width(), component.size.height() );
1134 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1135
1136 // add buffer
1137 sizeOut += bufferSize * 2;
1138 }
1139 }
1140
1141 // don't bother rendering symbols smaller than 1x1 pixels in size
1142 // TODO: add option to not show any svgs under/over a certain size
1143 if ( sizeOut < 1.0 )
1144 return;
1145
1146 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1147 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1148 {
1149 QVariantMap map; // for SVG symbology marker
1150 map[u"name"_s] = background.svgFile().trimmed();
1151 map[u"size"_s] = QString::number( sizeOut );
1152 map[u"size_unit"_s] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1153 map[u"angle"_s] = QString::number( 0.0 ); // angle is handled by this local painter
1154
1155 // offset is handled by this local painter
1156 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1157 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1158 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1159 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1160
1161 map[u"fill"_s] = background.fillColor().name();
1162 map[u"outline"_s] = background.strokeColor().name();
1163 map[u"outline-width"_s] = QString::number( background.strokeWidth() );
1164 map[u"outline_width_unit"_s] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1165
1167 {
1168 QgsTextShadowSettings shadow = format.shadow();
1169 // configure SVG shadow specs
1170 QVariantMap shdwmap( map );
1171 shdwmap[u"fill"_s] = shadow.color().name();
1172 shdwmap[u"outline"_s] = shadow.color().name();
1173 shdwmap[u"size"_s] = QString::number( sizeOut );
1174
1175 // store SVG's drawing in QPicture for drop shadow call
1176 QPicture svgPict;
1177 QPainter svgp;
1178 svgp.begin( &svgPict );
1179
1180 // draw shadow symbol
1181
1182 // clone current render context map unit/mm conversion factors, but not
1183 // other map canvas parameters, then substitute this painter for use in symbology painting
1184 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1185 // but will be created relative to the SVG's computed size, not the current map canvas
1186 QgsRenderContext shdwContext;
1187 shdwContext.setMapToPixel( context.mapToPixel() );
1188 shdwContext.setScaleFactor( context.scaleFactor() );
1189 shdwContext.setPainter( &svgp );
1190
1191 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1192 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1193 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1194
1195 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1196 svgp.end();
1197
1198 component.picture = svgPict;
1199 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1200 component.pictureBuffer = 0.0;
1201
1202 component.size = QSizeF( sizeOut, sizeOut );
1203 component.offset = QPointF( 0.0, 0.0 );
1204
1205 // rotate about origin center of SVG
1206 QgsScopedQPainterState painterState( p );
1207 context.setPainterFlagsUsingContext( p );
1208
1209 p->translate( component.center.x(), component.center.y() );
1210 p->rotate( component.rotation );
1211 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1212 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1213 p->translate( QPointF( xoff, yoff ) );
1214 p->rotate( component.rotationOffset );
1215 p->translate( -sizeOut / 2, sizeOut / 2 );
1216
1217 drawShadow( context, component, format );
1218 }
1219 renderedSymbol.reset();
1220
1221 QgsSymbolLayer *symL = QgsSvgMarkerSymbolLayer::create( map );
1222 renderedSymbol = std::make_unique<QgsMarkerSymbol>( QgsSymbolLayerList() << symL );
1223 }
1224 else
1225 {
1226 renderedSymbol.reset( background.markerSymbol()->clone() );
1227 renderedSymbol->setSize( sizeOut );
1228 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1229 }
1230
1231 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1232
1233 // draw the actual symbol
1234 QgsScopedQPainterState painterState( p );
1235 context.setPainterFlagsUsingContext( p );
1236
1238 {
1239 p->setCompositionMode( background.blendMode() );
1240 }
1241 p->translate( component.center.x(), component.center.y() );
1242 p->rotate( component.rotation );
1243 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1244 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1245 p->translate( QPointF( xoff, yoff ) );
1246 p->rotate( component.rotationOffset );
1247
1248 const QgsFeature f = context.expressionContext().feature();
1249 renderedSymbol->startRender( context, context.expressionContext().fields() );
1250 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1251 renderedSymbol->stopRender( context );
1252 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1253
1254 break;
1255 }
1256
1261 {
1262 double w = component.size.width();
1263 double h = component.size.height();
1264
1265 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1266 {
1267 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1268 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), background.sizeMapUnitScale() );
1269 }
1270 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1271 {
1272 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1273 {
1274 if ( w > h )
1275 h = w;
1276 else if ( h > w )
1277 w = h;
1278 }
1279 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1280 {
1281 // start with label bound by circle
1282 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1283 w = h;
1284 }
1285 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1286 {
1287 // start with label bound by ellipse
1288 h = h * M_SQRT1_2 * 2;
1289 w = w * M_SQRT1_2 * 2;
1290 }
1291
1292 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1293 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), background.sizeMapUnitScale() );
1294
1295 w += bufferWidth * 2;
1296 h += bufferHeight * 2;
1297 }
1298
1299 // offsets match those of symbology: -x = left, -y = up
1300 QRectF rect( -w / 2.0, -h / 2.0, w, h );
1301
1302 if ( rect.isNull() )
1303 return;
1304
1305 QgsScopedQPainterState painterState( p );
1306 context.setPainterFlagsUsingContext( p );
1307
1308 p->translate( QPointF( component.center.x(), component.center.y() ) );
1309 p->rotate( component.rotation );
1310 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1311 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1312 p->translate( QPointF( xoff, yoff ) );
1313 p->rotate( component.rotationOffset );
1314
1315 QPainterPath path;
1316
1317 // Paths with curves must be enlarged before conversion to QPolygonF, or
1318 // the curves are approximated too much and appear jaggy
1319 QTransform t = QTransform::fromScale( 10, 10 );
1320 // inverse transform used to scale created polygons back to expected size
1321 QTransform ti = t.inverted();
1322
1324 {
1325 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1326 {
1327 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1328 }
1329 else
1330 {
1331 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1332 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1333 path.addRoundedRect( rect, xRadius, yRadius );
1334 }
1335 }
1337 {
1338 path.addEllipse( rect );
1339 }
1340 QPolygonF tempPolygon = path.toFillPolygon( t );
1341 QPolygonF polygon = ti.map( tempPolygon );
1342 QPicture shapePict;
1343 QPainter *oldp = context.painter();
1344 QPainter shapep;
1345
1346 shapep.begin( &shapePict );
1347 context.setPainter( &shapep );
1348
1349 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1350 renderedSymbol.reset( background.fillSymbol()->clone() );
1351 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1352
1353 const QgsFeature f = context.expressionContext().feature();
1354 renderedSymbol->startRender( context, context.expressionContext().fields() );
1355 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1356 renderedSymbol->stopRender( context );
1357
1358 shapep.end();
1359 context.setPainter( oldp );
1360
1362 {
1363 component.picture = shapePict;
1364 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1365
1366 component.size = rect.size();
1367 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1368 drawShadow( context, component, format );
1369 }
1370
1372 {
1373 p->setCompositionMode( background.blendMode() );
1374 }
1375
1376 // scale for any print output or image saving @ specific dpi
1377 p->scale( component.dpiRatio, component.dpiRatio );
1379 p->drawPicture( 0, 0, shapePict );
1380 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1381 break;
1382 }
1383 }
1384
1385 if ( tmpEffect )
1386 {
1387 tmpEffect->end( context );
1388 context.setPainter( prevP );
1389 }
1390}
1391
1392void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1393{
1394 QgsTextShadowSettings shadow = format.shadow();
1395
1396 QPainter *p = context.painter();
1397 const double componentWidth = component.size.width();
1398 const double componentHeight = component.size.height();
1399 const double xOffset = component.offset.x();
1400 const double yOffset = component.offset.y();
1401 double pictbuffer = component.pictureBuffer;
1402
1403 // generate pixmap representation of label component drawing
1404 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1405
1406 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1407 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage ? fontSize * shadow.blurRadius() / 100
1408 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1409 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1410 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1411
1412 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1413 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1414 double blurBufferClippingScale = 3.75;
1415 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1416
1417 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ), componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ), QImage::Format_ARGB32_Premultiplied );
1418
1419 // TODO: add labeling gui option to not show any shadows under/over a certain size
1420 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1421 int minBlurImgSize = 1;
1422 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1423 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1424 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1425 int maxBlurImgSize = 40000;
1426 if ( blurImg.isNull() || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize ) || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1427 return;
1428
1429 blurImg.fill( QColor( Qt::transparent ).rgba() );
1430 QPainter pictp;
1431 if ( !pictp.begin( &blurImg ) )
1432 return;
1433 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1434 QPointF imgOffset( blurbuffer + pictbuffer + xOffset, blurbuffer + pictbuffer + componentHeight + yOffset );
1435
1436 pictp.drawPicture( imgOffset, component.picture );
1437
1438 // overlay shadow color
1439 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1440 pictp.fillRect( blurImg.rect(), shadow.color() );
1441 pictp.end();
1442
1443 // blur the QImage in-place
1444 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1445 {
1446 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1447 }
1448
1449#if 0
1450 // debug rect for QImage shadow registration and clipping visualization
1451 QPainter picti;
1452 picti.begin( &blurImg );
1453 picti.setBrush( Qt::Dense7Pattern );
1454 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1455 imgPen.setWidth( 1 );
1456 picti.setPen( imgPen );
1457 picti.setOpacity( 0.1 );
1458 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1459 picti.end();
1460#endif
1461
1462 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage ? fontSize * shadow.offsetDistance() / 100
1463 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1464 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1465 if ( shadow.offsetGlobal() )
1466 {
1467 // TODO: check for differences in rotation origin and cw/ccw direction,
1468 // when this shadow function is used for something other than labels
1469
1470 // it's 0-->cw-->360 for labels
1471 //QgsDebugMsgLevel( u"Shadow aggregated label rotation (degrees): %1"_s.arg( component.rotation() + component.rotationOffset() ), 4 );
1472 angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1473 }
1474
1475 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ), -offsetDist * std::sin( angleRad + M_PI_2 ) );
1476
1477 p->save();
1478 context.setPainterFlagsUsingContext( p );
1479 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1480 // corresponding flag in the render context instead...
1481 p->setRenderHint( QPainter::SmoothPixmapTransform );
1483 {
1484 p->setCompositionMode( shadow.blendMode() );
1485 }
1486 p->setOpacity( shadow.opacity() );
1487
1488 double scale = shadow.scale() / 100.0;
1489 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1490 p->scale( scale, scale );
1491 if ( component.useOrigin )
1492 {
1493 p->translate( component.origin.x(), component.origin.y() );
1494 }
1495 p->translate( transPt );
1496 p->translate( -imgOffset.x(), -imgOffset.y() );
1497 p->drawImage( 0, 0, blurImg );
1498 p->restore();
1499
1500 // debug rects
1501#if 0
1502 // draw debug rect for QImage painting registration
1503 p->save();
1504 p->setBrush( Qt::NoBrush );
1505 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1506 imgPen.setWidth( 2 );
1507 imgPen.setStyle( Qt::DashLine );
1508 p->setPen( imgPen );
1509 p->scale( scale, scale );
1510 if ( component.useOrigin() )
1511 {
1512 p->translate( component.origin().x(), component.origin().y() );
1513 }
1514 p->translate( transPt );
1515 p->translate( -imgOffset.x(),
1516 -imgOffset.y() );
1517 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1518 p->restore();
1519
1520 // draw debug rect for passed in component dimensions
1521 p->save();
1522 p->setBrush( Qt::NoBrush );
1523 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1524 componentRectPen.setWidth( 1 );
1525 if ( component.useOrigin() )
1526 {
1527 p->translate( component.origin().x(), component.origin().y() );
1528 }
1529 p->setPen( componentRectPen );
1530 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1531 p->restore();
1532#endif
1533}
1534
1535
1536void QgsTextRenderer::drawTextInternal(
1537 Qgis::TextComponents components,
1538 QgsRenderContext &context,
1539 const QgsTextFormat &format,
1540 const Component &component,
1541 const QgsTextDocument &document,
1542 const QgsTextDocumentMetrics &metrics,
1544 Qgis::TextVerticalAlignment vAlignment,
1546)
1547{
1548 if ( !context.painter() )
1549 {
1550 return;
1551 }
1552
1553 const double fontScale = calculateScaleFactorForFormat( context, format );
1554
1555 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1556 if ( mode == Qgis::TextLayoutMode::Labeling )
1557 {
1558 // label size has already been calculated using any symbology reference scale factor -- we need
1559 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1560 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1561 }
1562
1563 if ( metrics.isNullFontSize() )
1564 return;
1565
1566 referenceScaleOverride.reset();
1567
1568 double rotation = 0;
1569 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1570 switch ( orientation )
1571 {
1573 {
1574 drawTextInternalHorizontal( context, format, components, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1575 break;
1576 }
1577
1580 {
1581 // TODO: vertical text renderer currently doesn't handle one-pass buffer + text drawing
1582 if ( components & Qgis::TextComponent::Buffer )
1583 drawTextInternalVertical( context, format, Qgis::TextComponent::Buffer, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1584 if ( components & Qgis::TextComponent::Text )
1585 drawTextInternalVertical( context, format, Qgis::TextComponent::Text, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1586 break;
1587 }
1588 }
1589}
1590
1591Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1592{
1593 rotation = -component.rotation * 180 / M_PI;
1594
1595 switch ( format.orientation() )
1596 {
1598 {
1599 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1600 if ( rotation >= -315 && rotation < -90 )
1601 {
1602 rotation -= 90;
1604 }
1605 else if ( rotation >= -90 && rotation < -45 )
1606 {
1607 rotation += 90;
1609 }
1610
1612 }
1613
1616 return format.orientation();
1617 }
1619}
1620
1621void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1622{
1623 const QString blockText = block.toPlainText();
1624 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1625 finder.toStart();
1626 int wordBoundaries = 0;
1627 while ( finder.toNextBoundary() != -1 )
1628 {
1629 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1630 wordBoundaries++;
1631 }
1632
1633 if ( wordBoundaries > 0 )
1634 {
1635 // word boundaries found => justify by padding word spacing
1636 extraWordSpace = spaceToDistribute / wordBoundaries;
1637 }
1638 else
1639 {
1640 // no word boundaries found => justify by letter spacing
1641 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1642 finder.toStart();
1643
1644 int graphemeBoundaries = 0;
1645 while ( finder.toNextBoundary() != -1 )
1646 {
1647 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1648 graphemeBoundaries++;
1649 }
1650
1651 if ( graphemeBoundaries > 0 )
1652 {
1653 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1654 }
1655 }
1656}
1657
1658void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1659{
1660 const double prevWordSpace = font.wordSpacing();
1661 font.setWordSpacing( prevWordSpace + extraWordSpace );
1662 const double prevLetterSpace = font.letterSpacing();
1663 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1664}
1665
1666
1667void QgsTextRenderer::renderBlockHorizontal(
1668 const QgsTextBlock &block,
1669 int blockIndex,
1670 const QgsTextDocumentMetrics &metrics,
1671 QgsRenderContext &context,
1672 const QgsTextFormat &format,
1673 QPainter *painter,
1674 bool forceRenderAsPaths,
1675 double fontScale,
1676 double extraWordSpace,
1677 double extraLetterSpace,
1679 DeferredRenderBlock *deferredRenderBlock
1680)
1681{
1682 if ( !metrics.isNullFontSize() )
1683 {
1684 double xOffset = 0;
1685 int fragmentIndex = 0;
1686 for ( const QgsTextFragment &fragment : block )
1687 {
1688 // draw text, QPainterPath method
1689 if ( !fragment.isWhitespace() && !fragment.isImage() )
1690 {
1691 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1692
1693 if ( !qgsDoubleNear( extraWordSpace, 0 ) || !qgsDoubleNear( extraLetterSpace, 0 ) )
1694 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1695
1696 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1697
1698 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1699 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1700
1701 if ( deferredRenderBlock )
1702 {
1703 DeferredRenderFragment renderFragment;
1704 renderFragment.color = textColor;
1705 if ( forceRenderAsPaths )
1706 {
1707 renderFragment.path.setFillRule( Qt::WindingFill );
1708 renderFragment.path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1709 }
1710 renderFragment.font = fragmentFont;
1711 renderFragment.point = QPointF( xOffset, yOffset );
1712 renderFragment.text = fragment.text();
1713 deferredRenderBlock->fragments.append( renderFragment );
1714 }
1715 else if ( forceRenderAsPaths )
1716 {
1717 painter->setBrush( textColor );
1718 QPainterPath path;
1719 path.setFillRule( Qt::WindingFill );
1720 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1721 painter->drawPath( path );
1722 }
1723 else
1724 {
1725 painter->setPen( textColor );
1726 painter->setFont( fragmentFont );
1727 painter->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1728 }
1729 }
1730 else if ( fragment.isImage() )
1731 {
1732 bool fitsInCache = false;
1733 const double imageWidth = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1734 const double imageHeight = metrics.fragmentFixedHeight( blockIndex, fragmentIndex, mode ) * fontScale;
1735
1736 const QImage image = QgsApplication::imageCache()->pathAsImage(
1737 fragment.characterFormat().imagePath(),
1738 QSize( static_cast< int >( std::round( imageWidth ) ), static_cast< int >( std::round( imageHeight ) ) ),
1739 false,
1740 1,
1741 fitsInCache,
1743 );
1744 const double imageBaseline = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1745 const double yOffset = imageBaseline - image.height();
1746 if ( !image.isNull() )
1747 painter->drawImage( QPointF( xOffset, yOffset ), image );
1748 }
1749
1750 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1751 fragmentIndex++;
1752 }
1753 }
1754};
1755
1756bool QgsTextRenderer::usePathsToRender( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
1757{
1758 switch ( context.textRenderFormat() )
1759 {
1761 return true;
1763 return false;
1765 {
1766 // Prefer not to use paths -- but certain conditions will require us to use them
1767 if ( format.buffer().enabled() )
1768 {
1769 // text buffer requires use of paths
1770 // TODO: this was the original cause of use switching from text to paths by default,
1771 // but that was way back in the 2.0 days and maybe the Qt issues have now been fixed?
1772 return true;
1773 }
1774
1775 // underline/overline/strikethrough looks different between path/non-path renders.
1776 // TODO: validate which is correct. For now, maintain default appearance from before this code
1777 // was introduced
1778 if ( format.font().underline() || format.font().overline() || format.font().strikeOut() || std::any_of( document.begin(), document.end(), []( const QgsTextBlock &block ) {
1779 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment &fragment ) {
1780 return fragment.characterFormat().underline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1781 || fragment.characterFormat().overline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1782 || fragment.characterFormat().strikeOut() == QgsTextCharacterFormat::BooleanValue::SetTrue;
1783 } );
1784 } ) )
1785 return true;
1786
1787 return false;
1788 }
1789 }
1791}
1792
1793bool QgsTextRenderer::usePictureToRender( const QgsRenderContext &, const QgsTextFormat &, const QgsTextDocument &document )
1794{
1795 return std::any_of( document.begin(), document.end(), []( const QgsTextBlock &block ) {
1796 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment &fragment ) { return fragment.isImage(); } );
1797 } );
1798}
1799
1800QVector< QgsTextRenderer::BlockMetrics > QgsTextRenderer::calculateBlockMetrics(
1801 const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode, double targetWidth, const Qgis::TextHorizontalAlignment hAlignment
1802)
1803{
1804 QVector< BlockMetrics > blockMetrics;
1805 blockMetrics.reserve( document.size() );
1806
1807 int blockIndex = 0;
1808 for ( const QgsTextBlock &block : document )
1809 {
1810 Qgis::TextHorizontalAlignment blockAlignment = hAlignment;
1811 if ( block.blockFormat().hasHorizontalAlignmentSet() )
1812 blockAlignment = block.blockFormat().horizontalAlignment();
1813 const bool adjustForAlignment = blockAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || document.size() > 1 );
1814
1815 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 ) || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1816
1817 BlockMetrics thisBlockMetrics;
1818 // figure x offset for horizontal alignment of multiple lines
1819 thisBlockMetrics.width = metrics.blockWidth( blockIndex );
1820
1821 if ( adjustForAlignment )
1822 {
1823 double blockWidthDiff = 0;
1824 switch ( blockAlignment )
1825 {
1827 blockWidthDiff = ( targetWidth - thisBlockMetrics.width - metrics.blockLeftMargin( blockIndex ) - metrics.blockRightMargin( blockIndex ) ) * 0.5 + metrics.blockLeftMargin( blockIndex );
1828 break;
1829
1831 blockWidthDiff = targetWidth - thisBlockMetrics.width - metrics.blockRightMargin( blockIndex );
1832 break;
1833
1835 if ( !isFinalLineInParagraph && targetWidth > thisBlockMetrics.width )
1836 {
1837 calculateExtraSpacingForLineJustification( targetWidth - thisBlockMetrics.width, block, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace );
1838 thisBlockMetrics.width = targetWidth;
1839 }
1840 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1841 break;
1842
1844 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1845 break;
1846 }
1847
1848 switch ( mode )
1849 {
1854 thisBlockMetrics.xOffset = blockWidthDiff;
1855 break;
1856
1858 {
1859 switch ( blockAlignment )
1860 {
1862 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth;
1863 break;
1864
1866 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth / 2.0;
1867 break;
1868
1871 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1872 break;
1873 }
1874 }
1875 break;
1876 }
1877 }
1878 else if ( blockAlignment == Qgis::TextHorizontalAlignment::Left || blockAlignment == Qgis::TextHorizontalAlignment::Justify )
1879 {
1880 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1881 }
1882
1883 switch ( mode )
1884 {
1888 thisBlockMetrics.backgroundWidth = targetWidth;
1889 thisBlockMetrics.backgroundXOffset = 0;
1890 break;
1893 thisBlockMetrics.backgroundWidth = thisBlockMetrics.width;
1894 thisBlockMetrics.backgroundXOffset = thisBlockMetrics.xOffset;
1895 break;
1896 }
1897
1898 blockMetrics << thisBlockMetrics;
1899 blockIndex++;
1900 }
1901 return blockMetrics;
1902}
1903
1904QBrush QgsTextRenderer::createBrushForPath( QgsRenderContext &context, const QString &path )
1905{
1906 bool fitsInCache = false;
1907 // use original image size
1908 const QSize imageSize = QgsApplication::imageCache()->originalSize( path, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1909 // TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
1910 const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
1911 const double pixelsPerMm = context.scaleFactor();
1912 const double imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
1913 const double imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
1914 QBrush res;
1915 if ( imageWidth == 0 || imageHeight == 0 )
1916 return res;
1917 const QImage image
1919 ->pathAsImage( path, QSize( static_cast< int >( std::round( imageWidth ) ), static_cast< int >( std::round( imageHeight ) ) ), false, 1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1920
1921 if ( !image.isNull() )
1922 {
1923 res.setTextureImage( image );
1924 }
1925 return res;
1926}
1927
1928void QgsTextRenderer::renderDocumentBackgrounds(
1929 QgsRenderContext &context,
1930 const QgsTextDocument &document,
1931 const QgsTextDocumentMetrics &metrics,
1932 const Component &component,
1933 const QVector< QgsTextRenderer::BlockMetrics > &blockMetrics,
1935 double verticalAlignOffset,
1936 double rotation
1937)
1938{
1939 int blockIndex = 0;
1940 context.painter()->translate( component.origin );
1941 if ( !qgsDoubleNear( rotation, 0.0 ) )
1942 context.painter()->rotate( rotation );
1943
1944 context.painter()->setPen( Qt::NoPen );
1945 context.painter()->setBrush( Qt::NoBrush );
1946 for ( const QgsTextBlock &block : document )
1947 {
1948 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1949 const double blockMaximumDescent = metrics.blockMaximumDescent( blockIndex );
1950 const double blockMaximumAscent = metrics.blockMaximumAscent( blockIndex );
1951
1952 if ( block.blockFormat().hasBackground() )
1953 {
1954 QBrush backgroundBrush = block.blockFormat().backgroundBrush();
1955 if ( !block.blockFormat().backgroundImagePath().isEmpty() )
1956 {
1957 const QBrush backgroundImageBrush = createBrushForPath( context, block.blockFormat().backgroundImagePath() );
1958 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1959 backgroundBrush = backgroundImageBrush;
1960 }
1961
1962 context.painter()->setBrush( backgroundBrush );
1963 context.painter()->drawRect(
1964 QRectF( blockMetrics[blockIndex].backgroundXOffset, baseLineOffset - blockMaximumAscent, blockMetrics[blockIndex].backgroundWidth, blockMaximumDescent + blockMaximumAscent )
1965 );
1966 }
1967
1968 double xOffset = 0;
1969 int fragmentIndex = 0;
1970
1971 for ( const QgsTextFragment &fragment : block )
1972 {
1973 const double horizontalAdvance = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
1974 const double ascent = metrics.fragmentAscent( blockIndex, fragmentIndex, mode );
1975 const double descent = metrics.fragmentDescent( blockIndex, fragmentIndex, mode );
1976
1977 if ( fragment.characterFormat().hasBackground() )
1978 {
1979 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1980
1981 QBrush backgroundBrush = fragment.characterFormat().backgroundBrush();
1982 if ( !fragment.characterFormat().backgroundImagePath().isEmpty() )
1983 {
1984 const QBrush backgroundImageBrush = createBrushForPath( context, fragment.characterFormat().backgroundImagePath() );
1985 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1986 backgroundBrush = backgroundImageBrush;
1987 }
1988
1989 context.painter()->setBrush( backgroundBrush );
1990 context.painter()->drawRect( QRectF( blockMetrics[blockIndex].xOffset + xOffset, baseLineOffset + verticalAlignOffset + yOffset - ascent, horizontalAdvance, ascent + descent ) );
1991 }
1992
1993 xOffset += horizontalAdvance;
1994 fragmentIndex++;
1995 }
1996
1997 blockIndex++;
1998 }
1999
2000 context.painter()->setBrush( Qt::NoBrush );
2001
2002 if ( !qgsDoubleNear( rotation, 0.0 ) )
2003 context.painter()->rotate( -rotation );
2004 context.painter()->translate( -component.origin );
2005}
2006
2007void QgsTextRenderer::drawTextInternalHorizontal(
2008 QgsRenderContext &context,
2009 const QgsTextFormat &format,
2010 Qgis::TextComponents components,
2012 const Component &component,
2013 const QgsTextDocument &document,
2014 const QgsTextDocumentMetrics &metrics,
2015 double fontScale,
2016 const Qgis::TextHorizontalAlignment hAlignment,
2017 Qgis::TextVerticalAlignment vAlignment,
2018 double rotation
2019)
2020{
2021 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2022
2023 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
2024
2025 double targetWidth = 0.0;
2026 switch ( mode )
2027 {
2030 targetWidth = documentSize.width();
2031 break;
2032
2036 targetWidth = component.size.width();
2037 break;
2038 }
2039
2040 double verticalAlignOffset = 0;
2041
2042 if ( mode == Qgis::TextLayoutMode::Rectangle )
2043 {
2044 const double overallHeight = documentSize.height();
2045 switch ( vAlignment )
2046 {
2048 verticalAlignOffset = metrics.blockVerticalMargin( -1 );
2049 break;
2050
2052 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5 + metrics.blockVerticalMargin( -1 );
2053 break;
2054
2056 verticalAlignOffset = ( component.size.height() - overallHeight ) + metrics.blockVerticalMargin( -1 );
2057 break;
2058 }
2059 }
2060 else if ( mode == Qgis::TextLayoutMode::Point )
2061 {
2062 verticalAlignOffset = -metrics.blockVerticalMargin( document.size() - 1 );
2063 }
2064
2065 // should we use text or paths for this render?
2066 const bool usePathsForText = usePathsToRender( context, format, document );
2067
2068 // TODO -- maybe we can avoid the nested vector? Need to confirm whether painter rotation & translation can be
2069 // done ONCE only, upfront
2070 std::unique_ptr< std::vector< DeferredRenderBlock > > deferredBlocks;
2071
2072 // Depending on format settings, we may need to render in multiple passes. Eg buffer than text, or shadow than text.
2073 // We try to avoid this if possible as it requires more work, and just do a single pass, rendering text directly as we go.
2074 // If we need to do multi-pass rendering then we'll calculate paths ONCE upfront and defer actually rendering these.
2075 const bool requiresMultiPassRendering
2076 = ( components & Qgis::TextComponent::Buffer && format.buffer().enabled() )
2078 if ( requiresMultiPassRendering )
2079 {
2080 deferredBlocks = std::make_unique< std::vector< DeferredRenderBlock > >();
2081 deferredBlocks->reserve( document.size() );
2082 }
2083
2084 if ( ( components & Qgis::TextComponent::Buffer ) || ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2085 {
2086 const QVector< BlockMetrics > blockMetrics = calculateBlockMetrics( document, metrics, mode, targetWidth, hAlignment );
2087
2088 if ( document.hasBackgrounds() )
2089 {
2090 renderDocumentBackgrounds( context, document, metrics, component, blockMetrics, mode, verticalAlignOffset, rotation );
2091 }
2092
2093 int blockIndex = 0;
2094 for ( const QgsTextBlock &block : document )
2095 {
2096 const double blockHeight = metrics.blockHeight( blockIndex );
2097
2098 DeferredRenderBlock *deferredBlock = nullptr;
2099 if ( requiresMultiPassRendering && deferredBlocks )
2100 {
2101 deferredBlocks->emplace_back( DeferredRenderBlock() );
2102 deferredBlock = &deferredBlocks->back();
2103 deferredBlock->fragments.reserve( block.size() );
2104 }
2105
2106 QgsScopedQPainterState painterState( context.painter() );
2108 context.painter()->translate( component.origin );
2109 if ( !qgsDoubleNear( rotation, 0.0 ) )
2110 context.painter()->rotate( rotation );
2111
2112 // apply to the mask painter the same transformations
2113 if ( maskPainter )
2114 {
2115 maskPainter->save();
2116 maskPainter->translate( component.origin );
2117 if ( !qgsDoubleNear( rotation, 0.0 ) )
2118 maskPainter->rotate( rotation );
2119 }
2120
2121 const BlockMetrics thisBlockMetrics = blockMetrics[blockIndex];
2122 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
2123
2124 const QPointF blockOrigin( thisBlockMetrics.xOffset, baseLineOffset + verticalAlignOffset );
2125 if ( deferredBlock )
2126 deferredBlock->origin = blockOrigin;
2127 else
2128 context.painter()->translate( blockOrigin );
2129 if ( maskPainter )
2130 maskPainter->translate( blockOrigin );
2131
2132 Component subComponent;
2133 subComponent.block = block;
2134 subComponent.blockIndex = blockIndex;
2135 subComponent.size = QSizeF( thisBlockMetrics.width, blockHeight );
2136 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
2137 subComponent.rotation = -component.rotation * 180 / M_PI;
2138 subComponent.rotationOffset = 0.0;
2139 subComponent.extraWordSpacing = thisBlockMetrics.extraWordSpace * fontScale;
2140 subComponent.extraLetterSpacing = thisBlockMetrics.extraLetterSpace * fontScale;
2141 if ( deferredBlock )
2142 deferredBlock->component = subComponent;
2143
2144 // draw the mask below the text (for preview)
2145 if ( format.mask().enabled() )
2146 {
2147 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
2148 }
2149
2150 // if we are drawing both text + buffer, we'll need a path, as we HAVE to render buffers using paths
2151 const bool needsPaths = usePathsForText
2152 || ( ( components & Qgis::TextComponent::Buffer ) && format.buffer().enabled() )
2153 || ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() );
2154
2155 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2156 if ( mode == Qgis::TextLayoutMode::Labeling )
2157 {
2158 // label size has already been calculated using any symbology reference scale factor -- we need
2159 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2160 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2161 }
2162
2163 referenceScaleOverride.reset();
2164
2165 // now render the actual text
2167 {
2168 context.painter()->setCompositionMode( format.blendMode() );
2169 }
2170
2171 // scale for any print output or image saving @ specific dpi
2172 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2173
2174 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2175 context.painter()->setPen( Qt::NoPen );
2176 context.painter()->setBrush( Qt::NoBrush );
2177
2178 renderBlockHorizontal( block, blockIndex, metrics, context, format, context.painter(), needsPaths, fontScale, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace, mode, deferredBlock );
2179
2180 if ( maskPainter )
2181 maskPainter->restore();
2182
2183 blockIndex++;
2184 }
2185 }
2186
2187 if ( deferredBlocks )
2188 {
2189 renderDeferredBlocks( context, format, components, *deferredBlocks, usePathsForText, fontScale, component, rotation );
2190 }
2191}
2192
2193void QgsTextRenderer::renderDeferredBlocks(
2194 QgsRenderContext &context,
2195 const QgsTextFormat &format,
2196 Qgis::TextComponents components,
2197 const std::vector< DeferredRenderBlock > &deferredBlocks,
2198 bool usePathsForText,
2199 double fontScale,
2200 const Component &component,
2201 double rotation
2202)
2203{
2204 if ( format.buffer().enabled() && ( components & Qgis::TextComponent::Buffer ) )
2205 {
2206 renderDeferredBuffer( context, format, components, deferredBlocks, fontScale, component, rotation );
2207 }
2208
2209 if ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2210 {
2211 renderDeferredShadowForText( context, format, deferredBlocks, fontScale, component, rotation );
2212 // TODO: there's an optimisation opportunity here -- if we are ALSO rendering the text component,
2213 // we could move the actual text rendering into renderDeferredShadowForText and use the same
2214 // QPicture as we used for the shadow. But we'd need to ensure that all the settings
2215 // which control whether text is rendered as text or paths also also considered.
2216 }
2217
2218 if ( components & Qgis::TextComponent::Text )
2219 {
2220 renderDeferredText( context, deferredBlocks, usePathsForText, fontScale, component, rotation );
2221 }
2222}
2223
2224void QgsTextRenderer::renderDeferredShadowForText(
2225 QgsRenderContext &context, const QgsTextFormat &format, const std::vector< DeferredRenderBlock > &deferredBlocks, double fontScale, const Component &component, double rotation
2226)
2227{
2228 QgsScopedQPainterState painterState( context.painter() );
2230 context.painter()->translate( component.origin );
2231 if ( !qgsDoubleNear( rotation, 0.0 ) )
2232 context.painter()->rotate( rotation );
2233
2234 context.painter()->setPen( Qt::NoPen );
2235 context.painter()->setBrush( Qt::NoBrush );
2236
2237 for ( const DeferredRenderBlock &block : deferredBlocks )
2238 {
2239 Component subComponent = block.component;
2240
2241 QPainter painter( &subComponent.picture );
2242 painter.setPen( Qt::NoPen );
2243 painter.setBrush( Qt::NoBrush );
2244 painter.scale( 1 / fontScale, 1 / fontScale );
2245
2246 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2247 {
2248 if ( !fragment.path.isEmpty() )
2249 {
2250 painter.setBrush( fragment.color );
2251 painter.drawPath( fragment.path );
2252 }
2253 else
2254 {
2255 painter.setPen( fragment.color );
2256 painter.setFont( fragment.font );
2257 painter.drawText( fragment.point, fragment.text );
2258 }
2259 }
2260 painter.end();
2261
2262 subComponent.pictureBuffer = 1.0; // no pen width to deal with, but we'll add 1 px for antialiasing
2263 subComponent.origin = QPointF( 0.0, 0.0 );
2264 const QRectF pictureBoundingRect = subComponent.picture.boundingRect();
2265 subComponent.size = pictureBoundingRect.size();
2266 subComponent.offset = QPointF( -pictureBoundingRect.left(), -pictureBoundingRect.height() - pictureBoundingRect.top() );
2267
2268 context.painter()->translate( block.origin );
2269 drawShadow( context, subComponent, format );
2270 context.painter()->translate( -block.origin );
2271 }
2272}
2273
2274void QgsTextRenderer::renderDeferredBuffer(
2275 QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, const std::vector< DeferredRenderBlock > &deferredBlocks, double fontScale, const Component &component, double rotation
2276)
2277{
2278 QgsScopedQPainterState painterState( context.painter() );
2280
2281 // do we need a drop shadow effect on the buffer component? If so, we'll render the buffer to a QPicture first and then use this
2282 // to generate the shadow, and then render the QPicture as the buffer on top. If not, avoid the unwanted expense of the temporary QPicture
2283 // and render directly.
2284 const bool needsShadowOnBuffer = ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer );
2285 std::unique_ptr< QPicture > bufferPicture;
2286 std::unique_ptr< QPainter > bufferPainter;
2287 QPainter *prevPainter = context.painter();
2288 if ( needsShadowOnBuffer )
2289 {
2290 bufferPicture = std::make_unique< QPicture >();
2291 bufferPainter = std::make_unique< QPainter >( bufferPicture.get() );
2292 context.setPainter( bufferPainter.get() );
2293 }
2294
2295 std::unique_ptr< QgsPaintEffect > tmpEffect;
2296 if ( format.buffer().paintEffect() && format.buffer().paintEffect()->enabled() )
2297 {
2298 tmpEffect.reset( format.buffer().paintEffect()->clone() );
2299 tmpEffect->begin( context );
2300 }
2301
2302 QColor bufferColor = format.buffer().color();
2303 bufferColor.setAlphaF( format.buffer().opacity() );
2304 QPen pen( bufferColor );
2305 const QgsTextBufferSettings &buffer = format.buffer();
2306 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
2307 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
2308 pen.setWidthF( penSize * fontScale );
2309 pen.setJoinStyle( buffer.joinStyle() );
2310 context.painter()->setPen( pen );
2311
2312 // honor pref for whether to fill buffer interior
2313 if ( !buffer.fillBufferInterior() )
2314 {
2315 bufferColor.setAlpha( 0 );
2316 }
2317 context.painter()->setBrush( bufferColor );
2318
2319 context.painter()->translate( component.origin );
2320 if ( !qgsDoubleNear( rotation, 0.0 ) )
2321 context.painter()->rotate( rotation );
2322
2324 {
2325 context.painter()->setCompositionMode( format.buffer().blendMode() );
2326 }
2327
2328 for ( const DeferredRenderBlock &block : deferredBlocks )
2329 {
2330 context.painter()->translate( block.origin );
2331 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2332 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2333 {
2334 context.painter()->drawPath( fragment.path );
2335 }
2336 context.painter()->scale( fontScale, fontScale );
2337 context.painter()->translate( -block.origin );
2338 }
2339
2340 if ( tmpEffect )
2341 {
2342 tmpEffect->end( context );
2343 }
2344
2345 if ( needsShadowOnBuffer && bufferPicture )
2346 {
2347 bufferPainter->end();
2348 bufferPainter.reset();
2349 context.setPainter( prevPainter );
2350
2351 QgsTextRenderer::Component bufferComponent = component;
2352 bufferComponent.origin = QPointF( 0.0, 0.0 );
2353 bufferComponent.picture = *bufferPicture;
2354 bufferComponent.pictureBuffer = penSize / 2.0;
2355 const QRectF bufferBoundingBox = bufferPicture->boundingRect();
2356 bufferComponent.size = bufferBoundingBox.size();
2357 bufferComponent.offset = QPointF( -bufferBoundingBox.left(), -bufferBoundingBox.height() - bufferBoundingBox.top() );
2358
2359 drawShadow( context, bufferComponent, format );
2360
2361 // also draw buffer
2363 {
2364 context.painter()->setCompositionMode( buffer.blendMode() );
2365 }
2366
2367 // scale for any print output or image saving @ specific dpi
2368 context.painter()->scale( component.dpiRatio, component.dpiRatio );
2369 QgsPainting::drawPicture( context.painter(), QPointF( 0, 0 ), *bufferPicture );
2370 }
2371}
2372
2373void QgsTextRenderer::renderDeferredText(
2374 QgsRenderContext &context, const std::vector< DeferredRenderBlock > &deferredBlocks, bool usePathsForText, double fontScale, const Component &component, double rotation
2375)
2376{
2377 QgsScopedQPainterState painterState( context.painter() );
2379 context.painter()->translate( component.origin );
2380 if ( !qgsDoubleNear( rotation, 0.0 ) )
2381 context.painter()->rotate( rotation );
2382
2383 context.painter()->setPen( Qt::NoPen );
2384 context.painter()->setBrush( Qt::NoBrush );
2385
2386 // draw the text
2387 for ( const DeferredRenderBlock &block : deferredBlocks )
2388 {
2389 context.painter()->translate( block.origin );
2390 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2391
2392 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2393 {
2394 if ( usePathsForText )
2395 {
2396 context.painter()->setBrush( fragment.color );
2397 context.painter()->drawPath( fragment.path );
2398 }
2399 else
2400 {
2401 context.painter()->setPen( fragment.color );
2402 context.painter()->setFont( fragment.font );
2403 context.painter()->drawText( fragment.point, fragment.text );
2404 }
2405 }
2406
2407 context.painter()->scale( fontScale, fontScale );
2408 context.painter()->translate( -block.origin );
2409 }
2410}
2411
2412void QgsTextRenderer::drawTextInternalVertical(
2413 QgsRenderContext &context,
2414 const QgsTextFormat &format,
2415 Qgis::TextComponents components,
2417 const QgsTextRenderer::Component &component,
2418 const QgsTextDocument &document,
2419 const QgsTextDocumentMetrics &metrics,
2420 double fontScale,
2423 double rotation
2424)
2425{
2426 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2427 const QStringList textLines = document.toPlainText();
2428
2429 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2430 if ( mode == Qgis::TextLayoutMode::Labeling )
2431 {
2432 // label size has already been calculated using any symbology reference scale factor -- we need
2433 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2434 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2435 }
2436
2437 if ( metrics.isNullFontSize() )
2438 return;
2439
2440 referenceScaleOverride.reset();
2441
2442 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
2443 const double actualTextWidth = documentSize.width();
2444 double textRectWidth = 0.0;
2445
2446 switch ( mode )
2447 {
2450 textRectWidth = actualTextWidth;
2451 break;
2452
2456 textRectWidth = component.size.width();
2457 break;
2458 }
2459
2460 int maxLineLength = 0;
2461 for ( const QString &line : std::as_const( textLines ) )
2462 {
2463 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2464 }
2465
2466 const double actualLabelHeight = documentSize.height();
2467 int blockIndex = 0;
2468
2469 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2470
2471 for ( const QgsTextBlock &block : document )
2472 {
2473 QgsScopedQPainterState painterState( context.painter() );
2475
2476 context.painter()->translate( component.origin );
2477 if ( !qgsDoubleNear( rotation, 0.0 ) )
2478 context.painter()->rotate( rotation );
2479
2480 // apply to the mask painter the same transformations
2481 if ( maskPainter )
2482 {
2483 maskPainter->save();
2484 maskPainter->translate( component.origin );
2485 if ( !qgsDoubleNear( rotation, 0.0 ) )
2486 maskPainter->rotate( rotation );
2487 }
2488
2489 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2490
2491 // figure x offset of multiple lines
2492 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2493 if ( adjustForAlignment )
2494 {
2495 double hAlignmentOffset = 0;
2496 switch ( hAlignment )
2497 {
2499 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2500 break;
2501
2503 hAlignmentOffset = textRectWidth - actualTextWidth;
2504 break;
2505
2508 break;
2509 }
2510
2511 switch ( mode )
2512 {
2517 xOffset += hAlignmentOffset;
2518 break;
2519
2521 break;
2522 }
2523 }
2524
2525 double yOffset = 0.0;
2526 switch ( mode )
2527 {
2530 {
2531 if ( rotation >= -405 && rotation < -180 )
2532 {
2533 yOffset = 0;
2534 }
2535 else if ( rotation >= 0 && rotation < 45 )
2536 {
2537 xOffset -= actualTextWidth;
2538 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2539 }
2540 }
2541 else
2542 {
2543 yOffset = -actualLabelHeight;
2544 }
2545 break;
2546
2548 yOffset = -actualLabelHeight;
2549 break;
2550
2554 yOffset = 0;
2555 break;
2556 }
2557
2558 context.painter()->translate( QPointF( xOffset, yOffset ) );
2559
2560 double currentBlockYOffset = 0;
2561 int fragmentIndex = 0;
2562 for ( const QgsTextFragment &fragment : block )
2563 {
2564 QgsScopedQPainterState fragmentPainterState( context.painter() );
2565
2566 // apply some character replacement to draw symbols in vertical presentation
2567 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2568
2569 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2570
2571 QFontMetricsF fragmentMetrics( fragmentFont );
2572
2573 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2574 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2575
2576 Component subComponent;
2577 subComponent.block = QgsTextBlock( fragment );
2578 subComponent.blockIndex = blockIndex;
2579 subComponent.firstFragmentIndex = fragmentIndex;
2580 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2581 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2582 subComponent.rotation = -component.rotation * 180 / M_PI;
2583 subComponent.rotationOffset = 0.0;
2584
2585 // draw the mask below the text (for preview)
2586 if ( format.mask().enabled() )
2587 {
2588 // WARNING: totally broken! (has been since mask was introduced)
2589#if 0
2590 QgsTextRenderer::drawMask( context, subComponent, format );
2591#endif
2592 }
2593
2594 if ( components & Qgis::TextComponent::Buffer )
2595 {
2596 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2597 }
2598 if ( ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2599 {
2600 // draw text, QPainterPath method
2601 QPainterPath path;
2602 path.setFillRule( Qt::WindingFill );
2603 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2604 double partYOffset = 0.0;
2605 for ( const QString &part : parts )
2606 {
2607 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2608 partYOffset += fragmentMetrics.ascent() / fontScale;
2609 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2610 partYOffset += letterSpacing;
2611 }
2612
2613 // store text's drawing in QPicture for drop shadow call
2614 QPicture textPict;
2615 QPainter textp;
2616 textp.begin( &textPict );
2617 textp.setPen( Qt::NoPen );
2618 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2619 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2620 textp.setBrush( textColor );
2621 textp.scale( 1 / fontScale, 1 / fontScale );
2622 textp.drawPath( path );
2623
2624 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2625 // e.g. some capitalization options, but not others
2626 //textp.setFont( tmpLyr.textFont );
2627 //textp.setPen( tmpLyr.textColor );
2628 //textp.drawText( 0, 0, component.text() );
2629 textp.end();
2630
2631 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2632 {
2633 subComponent.picture = textPict;
2634 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2635 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2636 const double prevY = subComponent.offset.y();
2637 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2638 subComponent.useOrigin = true;
2639 QgsTextRenderer::drawShadow( context, subComponent, format );
2640 subComponent.useOrigin = false;
2641 subComponent.offset = QPointF( 0, prevY );
2642 }
2643
2644 // paint the text
2646 {
2647 context.painter()->setCompositionMode( format.blendMode() );
2648 }
2649
2650 // scale for any print output or image saving @ specific dpi
2651 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2652
2653 // TODO -- this should respect the context's TextRenderFormat
2654 // draw outlined text
2655 context.painter()->translate( 0, currentBlockYOffset );
2657 context.painter()->drawPicture( 0, 0, textPict );
2658 currentBlockYOffset += partYOffset;
2659 }
2660 fragmentIndex++;
2661 }
2662
2663 if ( maskPainter )
2664 maskPainter->restore();
2665 blockIndex++;
2666 }
2667}
2668
2670{
2672 return 1.0;
2673
2674 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2675
2676 // THESE THRESHOLDS MAY NEED TWEAKING!
2677
2678 // NOLINTBEGIN(bugprone-branch-clone)
2679
2680 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2681 // 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
2682 if ( pixelSize < 50 )
2683 return 200 / pixelSize;
2684 //... 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!
2685 // 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
2686 else if ( pixelSize > 200 )
2687 return 200 / pixelSize;
2688 else
2689 return 1.0;
2690
2691 // NOLINTEND(bugprone-branch-clone)
2692}
@ ForceVector
Always force vector-based rendering, even when the result will be visually different to a raster-base...
Definition qgis.h:2898
TextAnchorPoint
Anchor point of label text.
Definition qgis.h:1475
TextLayoutMode
Text layout modes.
Definition qgis.h:3099
@ Labeling
Labeling-specific layout mode.
Definition qgis.h:3102
@ Point
Text at point of origin layout mode.
Definition qgis.h:3101
@ RectangleAscentBased
Similar to Rectangle mode, but uses ascents only when calculating font and line heights.
Definition qgis.h:3104
@ RectangleCapHeightBased
Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line ...
Definition qgis.h:3103
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:3100
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3622
TextOrientation
Text orientations.
Definition qgis.h:3084
@ Vertical
Vertically oriented text.
Definition qgis.h:3086
@ RotationBased
Horizontally or vertically oriented text based on rotation (only available for map labeling).
Definition qgis.h:3087
@ Horizontal
Horizontally oriented text.
Definition qgis.h:3085
@ Round
Use rounded joins.
Definition qgis.h:2273
@ Normal
Adjacent characters are positioned in the standard way for text in the writing system in use.
Definition qgis.h:3176
@ SubScript
Characters are placed below the base line for normal text.
Definition qgis.h:3178
@ SuperScript
Characters are placed above the base line for normal text.
Definition qgis.h:3177
@ PreferText
Render text as text objects, unless doing so results in rendering artifacts or poor quality rendering...
Definition qgis.h:3027
@ AlwaysOutlines
Always render text using path objects (AKA outlines/curves). This setting guarantees the best quality...
Definition qgis.h:3021
@ AlwaysText
Always render text as text objects. While this mode preserves text objects as text for post-processin...
Definition qgis.h:3024
RenderUnit
Rendering size units.
Definition qgis.h:5650
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size).
Definition qgis.h:5654
@ Unknown
Mixed or unknown units.
Definition qgis.h:5657
@ MapUnits
Map units.
Definition qgis.h:5652
@ Pixels
Pixels.
Definition qgis.h:5653
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2956
@ RenderBlocking
Render and load remote sources in the same thread to ensure rendering remote sources (svg and images)...
Definition qgis.h:2953
TextVerticalAlignment
Text vertical alignment.
Definition qgis.h:3159
@ Bottom
Align to bottom.
Definition qgis.h:3162
@ Top
Align to top.
Definition qgis.h:3160
@ VerticalCenter
Center align.
Definition qgis.h:3161
QFlags< TextComponent > TextComponents
Text components.
Definition qgis.h:3129
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3140
@ Justify
Justify align.
Definition qgis.h:3144
@ Center
Center align.
Definition qgis.h:3142
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3619
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
Definition qgis.h:3201
TextComponent
Text components.
Definition qgis.h:3116
@ Shadow
Drop shadow.
Definition qgis.h:3120
@ Buffer
Buffer component.
Definition qgis.h:3118
@ Text
Text component.
Definition qgis.h:3117
@ Background
Background shape.
Definition qgis.h:3119
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
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.
QgsFillSymbol * clone() const override
Returns a deep copy of this symbol.
Does vector analysis using the GEOS library and handles import, export, and exception handling.
Definition qgsgeos.h:175
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, double targetDpi=96, int frameNumber=-1, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
Line string geometry type, with support for z-dimension and m-values.
static std::unique_ptr< 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.
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.
static void applyScaleFixForQPictureDpi(QPainter *painter)
Applies a workaround to a painter to avoid an issue with incorrect scaling when drawing QPictures.
static void drawPicture(QPainter *painter, const QPointF &point, const QPicture &picture)
Draws a picture onto a painter, correctly applying workarounds to avoid issues with incorrect scaling...
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 rendered 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.
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().
Qgis::RasterizedRenderingPolicy rasterizedRenderingPolicy() const
Returns the policy controlling when rasterisation of content during renders is permitted.
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
Qgis::RenderContextFlags flags() const
Returns combination of flags used for rendering.
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.
QgsMapUnitScale strokeWidthMapUnitScale() const
Returns the map unit scale object for the shape stroke width.
RotationType rotationType() const
Returns the method used for rotating the background shape.
QString svgFile() const
Returns the absolute path to the background SVG file, if set.
QSizeF size() const
Returns the size of the background shape.
QSizeF radii() const
Returns the radii used for rounding the corners of shapes.
QgsMapUnitScale radiiMapUnitScale() const
Returns the map unit scale object for the shape radii.
Qgis::RenderUnit radiiUnit() const
Returns the units used for the shape's radii.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the background shape.
@ SizeBuffer
Shape size is determined by adding a buffer margin around text.
bool enabled() const
Returns whether the background is enabled.
double opacity() const
Returns the background shape's opacity.
double rotation() const
Returns the rotation for the background shape, in degrees clockwise.
QColor fillColor() const
Returns the color used for filing the background shape.
SizeType sizeType() const
Returns the method used to determine the size of the background shape (e.g., fixed size or buffer aro...
Qgis::RenderUnit strokeWidthUnit() const
Returns the units used for the shape's stroke width.
ShapeType type() const
Returns the type of background shape (e.g., square, ellipse, SVG).
double strokeWidth() const
Returns the width of the shape's stroke (stroke).
@ ShapeSquare
Square - buffered sizes only.
Qgis::RenderUnit offsetUnit() const
Returns the units used for the shape's offset.
QColor strokeColor() const
Returns the color used for outlining the background shape.
QgsFillSymbol * fillSymbol() const
Returns the fill symbol to be rendered in the background.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the shape size.
Qgis::RenderUnit sizeUnit() const
Returns the units used for the shape's size.
@ RotationOffset
Shape rotation is offset from text rotation.
@ RotationFixed
Shape rotation is a fixed angle.
QgsMarkerSymbol * markerSymbol() const
Returns the marker symbol to be rendered in the background.
const QgsPaintEffect * paintEffect() const
Returns the current paint effect for the background shape.
QgsMapUnitScale offsetMapUnitScale() const
Returns the map unit scale object for the shape offset.
QPointF offset() const
Returns the offset used for drawing the background shape.
Represents a block of text consisting of one or more QgsTextFragment objects.
QString toPlainText() const
Converts the block to plain text.
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 ...
QColor textColor() const
Returns the character's text color, or an invalid color if no color override is set and the default f...
QString backgroundImagePath() const
Returns the path for the image to be used for rendering the background of the fragment.
QBrush backgroundBrush() const
Returns the brush used for rendering the background of the fragment.
QString imagePath() const
Returns the path to the image to render, if the format applies to a document image fragment.
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.
bool hasBackground() const
Returns true if the fragment has a background set.
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.
double fragmentDescent(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the descent of the fragment at the specified block and fragment index.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
double blockRightMargin(int blockIndex) const
Returns the margin for the right side of the specified block index.
static QgsTextDocumentMetrics calculateMetrics(const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor=1.0, const QgsTextDocumentRenderContext &documentContext=QgsTextDocumentRenderContext())
Returns precalculated text metrics for a text document, when rendered using the given base format and...
QFont fragmentFont(int blockIndex, int fragmentIndex) const
Returns the calculated font for the fragment at the specified block and fragment indices.
double blockMaximumCharacterWidth(int blockIndex) const
Returns the maximum character width for the specified block.
double baselineOffset(int blockIndex, Qgis::TextLayoutMode mode) const
Returns the offset from the top of the document to the text baseline for the given block index.
double fragmentFixedHeight(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the fixed height of the fragment at the specified block and fragment index,...
double blockLeftMargin(int blockIndex) const
Returns the margin for the left side of the specified block index.
double blockMaximumAscent(int blockIndex) const
Returns the maximum ascent encountered in the specified block.
double fragmentAscent(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the ascent of the fragment at the specified block and fragment index.
double blockHeight(int blockIndex) const
Returns the height of the block at the specified index.
double fragmentHorizontalAdvance(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the horizontal advance of the fragment at the specified block and fragment index.
bool isNullFontSize() const
Returns true if the metrics could not be calculated because the text format has a null font size.
const QgsTextDocument & document() const
Returns the document associated with the calculated metrics.
double blockWidth(int blockIndex) const
Returns the width of the block at the specified index.
double ascentOffset() const
Returns the ascent offset of the first block in the document.
double blockVerticalMargin(int blockIndex) const
Returns the vertical margin for the specified block index.
Encapsulates the context in which a text document is to be rendered.
void setFlags(Qgis::TextRendererFlags flags)
Sets associated text renderer flags.
void setMaximumWidth(double width)
Sets the maximum width (in painter units) for rendered text.
Represents a document consisting of one or more QgsTextBlock objects.
QStringList toPlainText() const
Returns a list of plain text lines of text representing the document.
int size() const
Returns the number of blocks in the document.
void append(const QgsTextBlock &block)
Appends a block to the document.
static QgsTextDocument fromTextAndFormat(const QStringList &lines, const QgsTextFormat &format)
Constructor for QgsTextDocument consisting of a set of lines, respecting settings from a text format.
void applyCapitalization(Qgis::Capitalization capitalization)
Applies a capitalization style to the document's text.
bool hasBackgrounds() const
Returns true if any blocks or fragments in the document have background brushes set.
Container for all settings relating to text rendering.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the size.
void updateDataDefinedProperties(QgsRenderContext &context)
Updates the format by evaluating current values of data defined properties.
QgsPropertyCollection & dataDefinedProperties()
Returns a reference to the format's property collection, used for data defined overrides.
QFont scaledFont(const QgsRenderContext &context, double scaleFactor=1.0, bool *isZeroSize=nullptr) const
Returns a font with the size scaled to match the format's size settings (including units and map unit...
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the text.
Qgis::Capitalization capitalization() const
Returns the text capitalization style.
QgsTextMaskSettings & mask()
Returns a reference to the masking settings.
QgsTextBackgroundSettings & background()
Returns a reference to the text background settings.
Qgis::RenderUnit sizeUnit() const
Returns the units for the size of rendered text.
double opacity() const
Returns the text's opacity.
Qgis::TextOrientation orientation() const
Returns the orientation of the text.
double size() const
Returns the size for rendered text.
QgsTextShadowSettings & shadow()
Returns a reference to the text drop shadow settings.
QColor color() const
Returns the color that text will be rendered in.
QFont font() const
Returns the font used for rendering text.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.
QString text() const
Returns the text content of the fragment.
const QgsTextCharacterFormat & characterFormat() const
Returns the character formatting for the fragment.
bool isImage() const
Returns true if the fragment represents an image.
bool isWhitespace() const
Returns true if the fragment consists of just whitespace characters, and does not contain any content...
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 ...
static std::unique_ptr< CurvePlacementProperties > generateCurvedTextPlacement(const QgsPrecalculatedTextMetrics &metrics, const QPolygonF &line, double offsetAlongLine, LabelLineDirection direction=RespectPainterOrientation, double maxConcaveAngle=-1, double maxConvexAngle=-1, Qgis::CurvedTextFlags flags=Qgis::CurvedTextFlags(), Qgis::TextAnchorPoint textAnchor=Qgis::TextAnchorPoint::StartOfText)
Calculates curved text placement properties.
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 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 void drawTextOnLine(const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &format, double offsetAlongLine=0, double offsetFromLine=0, Qgis::CurvedTextFlags flags=Qgis::CurvedTextFlag::UseBaselinePlacement|Qgis::CurvedTextFlag::TruncateStringWhenLineIsTooShort, Qgis::TextAnchorPoint textAnchor=Qgis::TextAnchorPoint::StartOfText)
Draws text along a line using the specified settings.
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.
static void drawDocumentOnLine(const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine=0, double offsetFromLine=0, Qgis::CurvedTextFlags flags=Qgis::CurvedTextFlag::UseBaselinePlacement|Qgis::CurvedTextFlag::TruncateStringWhenLineIsTooShort, Qgis::TextAnchorPoint textAnchor=Qgis::TextAnchorPoint::StartOfText)
Draws a text document along a line using the specified settings.
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:112
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define BUILTIN_UNREACHABLE
Definition qgis.h:8051
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7417
const char * finder(const char *name)
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30