QGIS API Documentation 4.3.0-Master (767b36bf018)
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 Component backgroundComponent = component;
475 if ( !qgsDoubleNear( rotation, 0.0 ) )
476 {
477 // get rotated label's center point
478
479 double xc = rect.width() / 2.0;
480 double yc = rect.height() / 2.0;
481
482 double angle = -rotation;
483 double xd = xc * std::cos( angle ) - yc * std::sin( angle );
484 double yd = xc * std::sin( angle ) + yc * std::cos( angle );
485
486 backgroundComponent.center = QPointF( backgroundComponent.origin.x() + xd, backgroundComponent.origin.y() + yd );
487 }
488 else
489 {
490 backgroundComponent.center = rect.center();
491 }
492
493 switch ( vAlignment )
494 {
496 break;
498 backgroundComponent.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
499 break;
501 backgroundComponent.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
502 break;
503 }
504
505 QgsTextRenderer::drawBackground( context, backgroundComponent, format, metrics, Qgis::TextLayoutMode::Rectangle );
506 }
507
508 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
509 {
510 return;
511 }
512
514 {
515 drawTextInternal( parts, context, format, component, document, metrics, alignment, vAlignment, mode );
516 }
517}
518
520 QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool
521)
522{
523 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
524 const double fontScale = calculateScaleFactorForFormat( context, format );
525 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
526
527 drawParts( origin, rotation, alignment, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Point );
528}
529
530void QgsTextRenderer::drawParts(
531 QPointF origin,
532 double rotation,
534 const QgsTextDocument &document,
535 const QgsTextDocumentMetrics &metrics,
536 QgsRenderContext &context,
537 const QgsTextFormat &format,
540)
541{
542 if ( !context.painter() )
543 {
544 return;
545 }
546
547 Component component;
548 component.dpiRatio = 1.0;
549 component.origin = origin;
550 component.rotation = rotation;
551 component.hAlign = alignment;
552
553 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
554 {
555 QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
556 }
557
558 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
559 {
560 return;
561 }
562
564 {
565 drawTextInternal( parts, context, format, component, document, metrics, alignment, Qgis::TextVerticalAlignment::Top, mode );
566 }
567}
568
569QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
570{
571 return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
572}
573
574double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
575{
576 QPainter *p = context.painter();
577
578 Qgis::TextOrientation orientation = format.orientation();
580 {
581 if ( component.rotation >= -315 && component.rotation < -90 )
582 {
584 }
585 else if ( component.rotation >= -90 && component.rotation < -45 )
586 {
588 }
589 else
590 {
592 }
593 }
594
595 QgsTextBufferSettings buffer = format.buffer();
596
597 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
598 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
599
600 const double scaleFactor = calculateScaleFactorForFormat( context, format );
601
602 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
603 if ( mode == Qgis::TextLayoutMode::Labeling )
604 {
605 // label size has already been calculated using any symbology reference scale factor -- we need
606 // to temporarily remove the reference scale here or we'll be applying the scaling twice
607 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
608 }
609
610 if ( metrics.isNullFontSize() )
611 return 0;
612
613 referenceScaleOverride.reset();
614
615 QPainterPath path;
616 path.setFillRule( Qt::WindingFill );
617 double advance = 0;
618 double height = component.size.height();
619 switch ( orientation )
620 {
622 {
623 // NOT SUPPORTED BY THIS METHOD ANYMORE -- buffer drawing is handled in drawTextInternalHorizontal since QGIS 3.42
624 break;
625 }
626
629 {
630 double partYOffset = component.offset.y() * scaleFactor;
631
632 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
633 double partLastDescent = 0;
634
635 int fragmentIndex = 0;
636 for ( const QgsTextFragment &fragment : component.block )
637 {
638 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
639 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
640
641 const QFontMetricsF fragmentMetrics( fragmentFont );
642
643 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
644
645 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
646 for ( const QString &part : parts )
647 {
648 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
649 partYOffset += fragmentMetrics.ascent() / scaleFactor;
650 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
651 partYOffset += letterSpacing;
652 }
653 partLastDescent = fragmentMetrics.descent() / scaleFactor;
654
655 fragmentIndex++;
656 }
657 height = partYOffset + partLastDescent;
658 advance = partYOffset - component.offset.y() * scaleFactor;
659 break;
660 }
661 }
662
663 QColor bufferColor = buffer.color();
664 bufferColor.setAlphaF( buffer.opacity() );
665 QPen pen( bufferColor );
666 pen.setWidthF( penSize * scaleFactor );
667 pen.setJoinStyle( buffer.joinStyle() );
668 QColor tmpColor( bufferColor );
669 // honor pref for whether to fill buffer interior
670 if ( !buffer.fillBufferInterior() )
671 {
672 tmpColor.setAlpha( 0 );
673 }
674
675 // store buffer's drawing in QPicture for drop shadow call
676 QPicture buffPict;
677 QPainter buffp;
678 buffp.begin( &buffPict );
679 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
680 {
681 context.setPainter( &buffp );
682 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
683
684 tmpEffect->begin( context );
685 context.painter()->setPen( pen );
686 context.painter()->setBrush( tmpColor );
687 if ( scaleFactor != 1.0 )
688 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
689 context.painter()->drawPath( path );
690 if ( scaleFactor != 1.0 )
691 context.painter()->scale( scaleFactor, scaleFactor );
692 tmpEffect->end( context );
693
694 context.setPainter( p );
695 }
696 else
697 {
698 if ( scaleFactor != 1.0 )
699 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
700 buffp.setPen( pen );
701 buffp.setBrush( tmpColor );
702 buffp.drawPath( path );
703 }
704 buffp.end();
705
707 {
708 QgsTextRenderer::Component bufferComponent = component;
709 bufferComponent.origin = QPointF( 0.0, 0.0 );
710 bufferComponent.picture = buffPict;
711 bufferComponent.pictureBuffer = penSize / 2.0;
712 bufferComponent.size.setHeight( height );
713
715 {
716 bufferComponent.offset.setY( -bufferComponent.size.height() );
717 }
718 drawShadow( context, bufferComponent, format );
719 }
720
721 QgsScopedQPainterState painterState( p );
722 context.setPainterFlagsUsingContext( p );
723
725 {
726 p->setCompositionMode( buffer.blendMode() );
727 }
728
729 // scale for any print output or image saving @ specific dpi
730 p->scale( component.dpiRatio, component.dpiRatio );
732 p->drawPicture( 0, 0, buffPict );
733
734 return advance / scaleFactor;
735}
736
737void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
738{
739 QgsTextMaskSettings mask = format.mask();
740
741 // the mask is drawn to a side painter
742 // or to the main painter for preview
743 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
744 if ( !p )
745 return;
746
747 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
748 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
749
750 // buffer: draw the text with a big pen
751 QPainterPath path;
752 path.setFillRule( Qt::WindingFill );
753
754 const double scaleFactor = calculateScaleFactorForFormat( context, format );
755
756 // TODO: vertical text mode was ignored when masking feature was added.
757 // Hopefully Oslandia come back and fix this? Hint hint...
758
759 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
760 if ( mode == Qgis::TextLayoutMode::Labeling )
761 {
762 // label size has already been calculated using any symbology reference scale factor -- we need
763 // to temporarily remove the reference scale here or we'll be applying the scaling twice
764 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
765 }
766
767 if ( metrics.isNullFontSize() )
768 return;
769
770 referenceScaleOverride.reset();
771
772 double xOffset = 0;
773 int fragmentIndex = 0;
774 for ( const QgsTextFragment &fragment : component.block )
775 {
776 if ( !fragment.isWhitespace() && !fragment.isImage() )
777 {
778 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
779
780 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
781 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
782 }
783
784 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
785 fragmentIndex++;
786 }
787
788 QColor bufferColor( Qt::gray );
789 bufferColor.setAlphaF( mask.opacity() );
790
791 QPen pen;
792 QBrush brush;
793 brush.setColor( bufferColor );
794 pen.setColor( bufferColor );
795 pen.setWidthF( penSize * scaleFactor );
796 pen.setJoinStyle( mask.joinStyle() );
797
798 QgsScopedQPainterState painterState( p );
799 context.setPainterFlagsUsingContext( p );
800
801 // scale for any print output or image saving @ specific dpi
802 p->scale( component.dpiRatio, component.dpiRatio );
803 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
804 {
805 QgsPainterSwapper swapper( context, p );
806 {
807 QgsEffectPainter effectPainter( context, mask.paintEffect() );
808 if ( scaleFactor != 1.0 )
809 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
810 context.painter()->setPen( pen );
811 context.painter()->setBrush( brush );
812 context.painter()->drawPath( path );
813 if ( scaleFactor != 1.0 )
814 context.painter()->scale( scaleFactor, scaleFactor );
815 }
816 }
817 else
818 {
819 if ( scaleFactor != 1.0 )
820 p->scale( 1 / scaleFactor, 1 / scaleFactor );
821 p->setPen( pen );
822 p->setBrush( brush );
823 p->drawPath( path );
824 if ( scaleFactor != 1.0 )
825 p->scale( scaleFactor, scaleFactor );
826 }
827}
828
829double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
830{
831 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( textLines, format );
832 if ( doc.size() == 0 )
833 return 0;
834
835 return textWidth( context, format, doc );
836}
837
838double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
839{
840 //calculate max width of text lines
841 const double scaleFactor = calculateScaleFactorForFormat( context, format );
842
843 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
844
845 // width doesn't change depending on layout mode, we can use anything here
846 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
847}
848
850 const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth
851)
852{
853 QStringList lines;
854 for ( const QString &line : textLines )
855 {
856 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
857 {
858 lines.append( wrappedText( context, line, maxLineWidth, format ) );
859 }
860 else
861 {
862 lines.append( line );
863 }
864 }
865
866 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( lines, format );
867 return textHeight( context, format, doc, mode );
868}
869
870double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
871{
872 const double scaleFactor = calculateScaleFactorForFormat( context, format );
873
874 bool isNullSize = false;
875 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
876 if ( isNullSize )
877 return 0;
878
879 const QFontMetrics fm( baseFont );
880 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
881
882 if ( !includeEffects )
883 return height;
884
885 double maxExtension = 0;
886 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
887 if ( format.buffer().enabled() )
888 {
889 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage ? fontSize * format.buffer().size() / 100
890 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
891 }
892 if ( format.shadow().enabled() )
893 {
894 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
895 ? fontSize * format.shadow().offsetDistance() / 100
896 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
897 )
898 + ( format.shadow().blurRadiusUnit() == Qgis::RenderUnit::Percentage
899 ? fontSize * format.shadow().blurRadius() / 100
900 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
901 );
902 }
903 if ( format.background().enabled() )
904 {
905 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
907 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
908 {
909 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
910 }
911 }
912
913 return height + maxExtension;
914}
915
916bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
917{
918 if ( qgsDoubleNear( width, 0.0 ) )
919 return false;
920
921 const QStringList multiLineSplit = text.split( '\n' );
922 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
923 return currentTextWidth > width;
924}
925
926QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
927{
928 const QStringList lines = text.split( '\n' );
929 QStringList outLines;
930 for ( const QString &line : lines )
931 {
932 if ( textRequiresWrapping( context, line, width, format ) )
933 {
934 //first step is to identify words which must be on their own line (too long to fit)
935 const QStringList words = line.split( ' ' );
936 QStringList linesToProcess;
937 QString wordsInCurrentLine;
938 for ( const QString &word : words )
939 {
940 if ( textRequiresWrapping( context, word, width, format ) )
941 {
942 //too long to fit
943 if ( !wordsInCurrentLine.isEmpty() )
944 linesToProcess << wordsInCurrentLine;
945 wordsInCurrentLine.clear();
946 linesToProcess << word;
947 }
948 else
949 {
950 if ( !wordsInCurrentLine.isEmpty() )
951 wordsInCurrentLine.append( ' ' );
952 wordsInCurrentLine.append( word );
953 }
954 }
955 if ( !wordsInCurrentLine.isEmpty() )
956 linesToProcess << wordsInCurrentLine;
957
958 for ( const QString &line : std::as_const( linesToProcess ) )
959 {
960 QString remainingText = line;
961 int lastPos = remainingText.lastIndexOf( ' ' );
962 while ( lastPos > -1 )
963 {
964 //check if remaining text is short enough to go in one line
965 if ( !textRequiresWrapping( context, remainingText, width, format ) )
966 {
967 break;
968 }
969
970 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
971 {
972 outLines << remainingText.left( lastPos );
973 remainingText = remainingText.mid( lastPos + 1 );
974 lastPos = 0;
975 }
976 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
977 }
978 outLines << remainingText;
979 }
980 }
981 else
982 {
983 outLines << line;
984 }
985 }
986
987 return outLines;
988}
989
990double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
991{
992 QgsTextDocument document = doc;
993 document.applyCapitalization( format.capitalization() );
994
995 //calculate max height of text lines
996 const double scaleFactor = calculateScaleFactorForFormat( context, format );
997
998 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
999 if ( metrics.isNullFontSize() )
1000 return 0;
1001
1002 return metrics.documentSize( mode, format.orientation() ).height();
1003}
1004
1005void QgsTextRenderer::drawBackground( QgsRenderContext &context, const QgsTextRenderer::Component &c, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
1006{
1007 Component component = c;
1008 QgsTextBackgroundSettings background = format.background();
1009
1010 QPainter *prevP = context.painter();
1011 QPainter *p = context.painter();
1012 std::unique_ptr< QgsPaintEffect > tmpEffect;
1013 if ( background.paintEffect() && background.paintEffect()->enabled() )
1014 {
1015 tmpEffect.reset( background.paintEffect()->clone() );
1016 tmpEffect->begin( context );
1017 p = context.painter();
1018 }
1019
1020 //QgsDebugMsgLevel( u"Background label rotation: %1"_s.arg( component.rotation() ), 4 );
1021
1022 // shared calculations between shapes and SVG
1023
1024 // configure angles, set component rotation and rotationOffset
1025 const double originAdjustRotationRadians = -component.rotation;
1027 {
1028 component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
1029 component.rotationOffset = background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
1030 }
1031 else // RotationFixed
1032 {
1033 component.rotation = 0.0; // don't use label's rotation
1034 component.rotationOffset = background.rotation();
1035 }
1036
1037 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1038
1039 if ( mode != Qgis::TextLayoutMode::Labeling )
1040 {
1041 // need to calculate size of text
1042 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1043 double width = documentSize.width();
1044 double height = documentSize.height();
1045
1046 switch ( mode )
1047 {
1051 switch ( component.hAlign )
1052 {
1055 component.center = QPointF( component.origin.x() + width / 2.0, component.origin.y() + height / 2.0 );
1056 break;
1057
1059 component.center = QPointF( component.origin.x() + component.size.width() / 2.0, component.origin.y() + height / 2.0 );
1060 break;
1061
1063 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0, component.origin.y() + height / 2.0 );
1064 break;
1065 }
1066 break;
1067
1069 {
1070 bool isNullSize = false;
1071 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1072 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1073 switch ( component.hAlign )
1074 {
1077 component.center = QPointF( component.origin.x() + width / 2.0, component.origin.y() - height / 2.0 + originAdjust );
1078 break;
1079
1081 component.center = QPointF( component.origin.x(), component.origin.y() - height / 2.0 + originAdjust );
1082 break;
1083
1085 component.center = QPointF( component.origin.x() - width / 2.0, component.origin.y() - height / 2.0 + originAdjust );
1086 break;
1087 }
1088
1089 // apply rotation to center point
1090 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1091 {
1092 const double dx = component.center.x() - component.origin.x();
1093 const double dy = component.center.y() - component.origin.y();
1094 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1095 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1096 }
1097 break;
1098 }
1099
1101 break;
1102 }
1103
1105 component.size = QSizeF( width, height );
1106 }
1107
1108 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1109
1110 switch ( background.type() )
1111 {
1114 {
1115 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1116
1117 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1118 return;
1119
1120 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1121 return;
1122
1123 double sizeOut = 0.0;
1124 {
1125 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1126
1127 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1128 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1129 {
1130 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1131 }
1132 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1133 {
1134 sizeOut = std::max( component.size.width(), component.size.height() );
1135 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1136
1137 // add buffer
1138 sizeOut += bufferSize * 2;
1139 }
1140 }
1141
1142 // don't bother rendering symbols smaller than 1x1 pixels in size
1143 // TODO: add option to not show any svgs under/over a certain size
1144 if ( sizeOut < 1.0 )
1145 return;
1146
1147 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1148 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1149 {
1150 QVariantMap map; // for SVG symbology marker
1151 map[u"name"_s] = background.svgFile().trimmed();
1152 map[u"size"_s] = QString::number( sizeOut );
1153 map[u"size_unit"_s] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1154 map[u"angle"_s] = QString::number( 0.0 ); // angle is handled by this local painter
1155
1156 // offset is handled by this local painter
1157 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1158 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1159 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1160 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1161
1162 map[u"fill"_s] = background.fillColor().name();
1163 map[u"outline"_s] = background.strokeColor().name();
1164 map[u"outline-width"_s] = QString::number( background.strokeWidth() );
1165 map[u"outline_width_unit"_s] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1166
1168 {
1169 QgsTextShadowSettings shadow = format.shadow();
1170 // configure SVG shadow specs
1171 QVariantMap shdwmap( map );
1172 shdwmap[u"fill"_s] = shadow.color().name();
1173 shdwmap[u"outline"_s] = shadow.color().name();
1174 shdwmap[u"size"_s] = QString::number( sizeOut );
1175
1176 // store SVG's drawing in QPicture for drop shadow call
1177 QPicture svgPict;
1178 QPainter svgp;
1179 svgp.begin( &svgPict );
1180
1181 // draw shadow symbol
1182
1183 // clone current render context map unit/mm conversion factors, but not
1184 // other map canvas parameters, then substitute this painter for use in symbology painting
1185 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1186 // but will be created relative to the SVG's computed size, not the current map canvas
1187 QgsRenderContext shdwContext;
1188 shdwContext.setMapToPixel( context.mapToPixel() );
1189 shdwContext.setScaleFactor( context.scaleFactor() );
1190 shdwContext.setPainter( &svgp );
1191
1192 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1193 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1194 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1195
1196 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1197 svgp.end();
1198
1199 component.picture = svgPict;
1200 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1201 component.pictureBuffer = 0.0;
1202
1203 component.size = QSizeF( sizeOut, sizeOut );
1204 component.offset = QPointF( 0.0, 0.0 );
1205
1206 // rotate about origin center of SVG
1207 QgsScopedQPainterState painterState( p );
1208 context.setPainterFlagsUsingContext( p );
1209
1210 p->translate( component.center.x(), component.center.y() );
1211 p->rotate( component.rotation );
1212 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1213 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1214 p->translate( QPointF( xoff, yoff ) );
1215 p->rotate( component.rotationOffset );
1216 p->translate( -sizeOut / 2, sizeOut / 2 );
1217
1218 drawShadow( context, component, format );
1219 }
1220 renderedSymbol.reset();
1221
1222 QgsSymbolLayer *symL = QgsSvgMarkerSymbolLayer::create( map );
1223 renderedSymbol = std::make_unique<QgsMarkerSymbol>( QgsSymbolLayerList() << symL );
1224 }
1225 else
1226 {
1227 renderedSymbol.reset( background.markerSymbol()->clone() );
1228 renderedSymbol->setSize( sizeOut );
1229 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1230 }
1231
1232 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1233
1234 // draw the actual symbol
1235 QgsScopedQPainterState painterState( p );
1236 context.setPainterFlagsUsingContext( p );
1237
1239 {
1240 p->setCompositionMode( background.blendMode() );
1241 }
1242 p->translate( component.center.x(), component.center.y() );
1243 p->rotate( component.rotation );
1244 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1245 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1246 p->translate( QPointF( xoff, yoff ) );
1247 p->rotate( component.rotationOffset );
1248
1249 const QgsFeature f = context.expressionContext().feature();
1250 renderedSymbol->startRender( context, context.expressionContext().fields() );
1251 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1252 renderedSymbol->stopRender( context );
1253 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1254
1255 break;
1256 }
1257
1262 {
1263 double w = component.size.width();
1264 double h = component.size.height();
1265
1266 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1267 {
1268 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1269 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), background.sizeMapUnitScale() );
1270 }
1271 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1272 {
1273 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1274 {
1275 if ( w > h )
1276 h = w;
1277 else if ( h > w )
1278 w = h;
1279 }
1280 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1281 {
1282 // start with label bound by circle
1283 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1284 w = h;
1285 }
1286 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1287 {
1288 // start with label bound by ellipse
1289 h = h * M_SQRT1_2 * 2;
1290 w = w * M_SQRT1_2 * 2;
1291 }
1292
1293 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1294 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), background.sizeMapUnitScale() );
1295
1296 w += bufferWidth * 2;
1297 h += bufferHeight * 2;
1298 }
1299
1300 // offsets match those of symbology: -x = left, -y = up
1301 QRectF rect( -w / 2.0, -h / 2.0, w, h );
1302
1303 if ( rect.isNull() )
1304 return;
1305
1306 QgsScopedQPainterState painterState( p );
1307 context.setPainterFlagsUsingContext( p );
1308
1309 p->translate( QPointF( component.center.x(), component.center.y() ) );
1310 p->rotate( component.rotation );
1311 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1312 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1313 p->translate( QPointF( xoff, yoff ) );
1314 p->rotate( component.rotationOffset );
1315
1316 QPainterPath path;
1317
1318 // Paths with curves must be enlarged before conversion to QPolygonF, or
1319 // the curves are approximated too much and appear jaggy
1320 QTransform t = QTransform::fromScale( 10, 10 );
1321 // inverse transform used to scale created polygons back to expected size
1322 QTransform ti = t.inverted();
1323
1325 {
1326 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1327 {
1328 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1329 }
1330 else
1331 {
1332 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1333 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1334 path.addRoundedRect( rect, xRadius, yRadius );
1335 }
1336 }
1338 {
1339 path.addEllipse( rect );
1340 }
1341 QPolygonF tempPolygon = path.toFillPolygon( t );
1342 QPolygonF polygon = ti.map( tempPolygon );
1343 QPicture shapePict;
1344 QPainter *oldp = context.painter();
1345 QPainter shapep;
1346
1347 shapep.begin( &shapePict );
1348 context.setPainter( &shapep );
1349
1350 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1351 renderedSymbol.reset( background.fillSymbol()->clone() );
1352 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1353
1354 const QgsFeature f = context.expressionContext().feature();
1355 renderedSymbol->startRender( context, context.expressionContext().fields() );
1356 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1357 renderedSymbol->stopRender( context );
1358
1359 shapep.end();
1360 context.setPainter( oldp );
1361
1363 {
1364 component.picture = shapePict;
1365 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1366
1367 component.size = rect.size();
1368 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1369 drawShadow( context, component, format );
1370 }
1371
1373 {
1374 p->setCompositionMode( background.blendMode() );
1375 }
1376
1377 // scale for any print output or image saving @ specific dpi
1378 p->scale( component.dpiRatio, component.dpiRatio );
1380 p->drawPicture( 0, 0, shapePict );
1381 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1382 break;
1383 }
1384 }
1385
1386 if ( tmpEffect )
1387 {
1388 tmpEffect->end( context );
1389 context.setPainter( prevP );
1390 }
1391}
1392
1393void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1394{
1395 QgsTextShadowSettings shadow = format.shadow();
1396
1397 QPainter *p = context.painter();
1398 const double componentWidth = component.size.width();
1399 const double componentHeight = component.size.height();
1400 const double xOffset = component.offset.x();
1401 const double yOffset = component.offset.y();
1402 double pictbuffer = component.pictureBuffer;
1403
1404 // generate pixmap representation of label component drawing
1405 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1406
1407 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1408 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage ? fontSize * shadow.blurRadius() / 100
1409 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1410 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1411 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1412
1413 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1414 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1415 double blurBufferClippingScale = 3.75;
1416 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1417
1418 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ), componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ), QImage::Format_ARGB32_Premultiplied );
1419
1420 // TODO: add labeling gui option to not show any shadows under/over a certain size
1421 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1422 int minBlurImgSize = 1;
1423 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1424 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1425 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1426 int maxBlurImgSize = 40000;
1427 if ( blurImg.isNull() || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize ) || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1428 return;
1429
1430 blurImg.fill( QColor( Qt::transparent ).rgba() );
1431 QPainter pictp;
1432 if ( !pictp.begin( &blurImg ) )
1433 return;
1434 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1435 QPointF imgOffset( blurbuffer + pictbuffer + xOffset, blurbuffer + pictbuffer + componentHeight + yOffset );
1436
1437 pictp.drawPicture( imgOffset, component.picture );
1438
1439 // overlay shadow color
1440 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1441 pictp.fillRect( blurImg.rect(), shadow.color() );
1442 pictp.end();
1443
1444 // blur the QImage in-place
1445 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1446 {
1447 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1448 }
1449
1450#if 0
1451 // debug rect for QImage shadow registration and clipping visualization
1452 QPainter picti;
1453 picti.begin( &blurImg );
1454 picti.setBrush( Qt::Dense7Pattern );
1455 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1456 imgPen.setWidth( 1 );
1457 picti.setPen( imgPen );
1458 picti.setOpacity( 0.1 );
1459 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1460 picti.end();
1461#endif
1462
1463 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage ? fontSize * shadow.offsetDistance() / 100
1464 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1465 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1466 if ( shadow.offsetGlobal() )
1467 {
1468 // TODO: check for differences in rotation origin and cw/ccw direction,
1469 // when this shadow function is used for something other than labels
1470
1471 // it's 0-->cw-->360 for labels
1472 //QgsDebugMsgLevel( u"Shadow aggregated label rotation (degrees): %1"_s.arg( component.rotation() + component.rotationOffset() ), 4 );
1473 angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1474 }
1475
1476 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ), -offsetDist * std::sin( angleRad + M_PI_2 ) );
1477
1478 p->save();
1479 context.setPainterFlagsUsingContext( p );
1480 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1481 // corresponding flag in the render context instead...
1482 p->setRenderHint( QPainter::SmoothPixmapTransform );
1484 {
1485 p->setCompositionMode( shadow.blendMode() );
1486 }
1487 p->setOpacity( shadow.opacity() );
1488
1489 double scale = shadow.scale() / 100.0;
1490 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1491 p->scale( scale, scale );
1492 if ( component.useOrigin )
1493 {
1494 p->translate( component.origin.x(), component.origin.y() );
1495 }
1496 p->translate( transPt );
1497 p->translate( -imgOffset.x(), -imgOffset.y() );
1498 p->drawImage( 0, 0, blurImg );
1499 p->restore();
1500
1501 // debug rects
1502#if 0
1503 // draw debug rect for QImage painting registration
1504 p->save();
1505 p->setBrush( Qt::NoBrush );
1506 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1507 imgPen.setWidth( 2 );
1508 imgPen.setStyle( Qt::DashLine );
1509 p->setPen( imgPen );
1510 p->scale( scale, scale );
1511 if ( component.useOrigin() )
1512 {
1513 p->translate( component.origin().x(), component.origin().y() );
1514 }
1515 p->translate( transPt );
1516 p->translate( -imgOffset.x(),
1517 -imgOffset.y() );
1518 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1519 p->restore();
1520
1521 // draw debug rect for passed in component dimensions
1522 p->save();
1523 p->setBrush( Qt::NoBrush );
1524 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1525 componentRectPen.setWidth( 1 );
1526 if ( component.useOrigin() )
1527 {
1528 p->translate( component.origin().x(), component.origin().y() );
1529 }
1530 p->setPen( componentRectPen );
1531 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1532 p->restore();
1533#endif
1534}
1535
1536
1537void QgsTextRenderer::drawTextInternal(
1538 Qgis::TextComponents components,
1539 QgsRenderContext &context,
1540 const QgsTextFormat &format,
1541 const Component &component,
1542 const QgsTextDocument &document,
1543 const QgsTextDocumentMetrics &metrics,
1545 Qgis::TextVerticalAlignment vAlignment,
1547)
1548{
1549 if ( !context.painter() )
1550 {
1551 return;
1552 }
1553
1554 const double fontScale = calculateScaleFactorForFormat( context, format );
1555
1556 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1557 if ( mode == Qgis::TextLayoutMode::Labeling )
1558 {
1559 // label size has already been calculated using any symbology reference scale factor -- we need
1560 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1561 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1562 }
1563
1564 if ( metrics.isNullFontSize() )
1565 return;
1566
1567 referenceScaleOverride.reset();
1568
1569 double rotation = 0;
1570 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1571 switch ( orientation )
1572 {
1574 {
1575 drawTextInternalHorizontal( context, format, components, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1576 break;
1577 }
1578
1581 {
1582 // TODO: vertical text renderer currently doesn't handle one-pass buffer + text drawing
1583 if ( components & Qgis::TextComponent::Buffer )
1584 drawTextInternalVertical( context, format, Qgis::TextComponent::Buffer, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1585 if ( components & Qgis::TextComponent::Text )
1586 drawTextInternalVertical( context, format, Qgis::TextComponent::Text, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1587 break;
1588 }
1589 }
1590}
1591
1592Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1593{
1594 rotation = -component.rotation * 180 / M_PI;
1595
1596 switch ( format.orientation() )
1597 {
1599 {
1600 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1601 if ( rotation >= -315 && rotation < -90 )
1602 {
1603 rotation -= 90;
1605 }
1606 else if ( rotation >= -90 && rotation < -45 )
1607 {
1608 rotation += 90;
1610 }
1611
1613 }
1614
1617 return format.orientation();
1618 }
1620}
1621
1622void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1623{
1624 const QString blockText = block.toPlainText();
1625 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1626 finder.toStart();
1627 int wordBoundaries = 0;
1628 while ( finder.toNextBoundary() != -1 )
1629 {
1630 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1631 wordBoundaries++;
1632 }
1633
1634 if ( wordBoundaries > 0 )
1635 {
1636 // word boundaries found => justify by padding word spacing
1637 extraWordSpace = spaceToDistribute / wordBoundaries;
1638 }
1639 else
1640 {
1641 // no word boundaries found => justify by letter spacing
1642 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1643 finder.toStart();
1644
1645 int graphemeBoundaries = 0;
1646 while ( finder.toNextBoundary() != -1 )
1647 {
1648 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1649 graphemeBoundaries++;
1650 }
1651
1652 if ( graphemeBoundaries > 0 )
1653 {
1654 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1655 }
1656 }
1657}
1658
1659void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1660{
1661 const double prevWordSpace = font.wordSpacing();
1662 font.setWordSpacing( prevWordSpace + extraWordSpace );
1663 const double prevLetterSpace = font.letterSpacing();
1664 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1665}
1666
1667
1668void QgsTextRenderer::renderBlockHorizontal(
1669 const QgsTextBlock &block,
1670 int blockIndex,
1671 const QgsTextDocumentMetrics &metrics,
1672 QgsRenderContext &context,
1673 const QgsTextFormat &format,
1674 QPainter *painter,
1675 bool forceRenderAsPaths,
1676 double fontScale,
1677 double extraWordSpace,
1678 double extraLetterSpace,
1680 DeferredRenderBlock *deferredRenderBlock
1681)
1682{
1683 if ( !metrics.isNullFontSize() )
1684 {
1685 double xOffset = 0;
1686 int fragmentIndex = 0;
1687 for ( const QgsTextFragment &fragment : block )
1688 {
1689 // draw text, QPainterPath method
1690 if ( !fragment.isWhitespace() && !fragment.isImage() )
1691 {
1692 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1693
1694 if ( !qgsDoubleNear( extraWordSpace, 0 ) || !qgsDoubleNear( extraLetterSpace, 0 ) )
1695 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1696
1697 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1698
1699 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1700 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1701
1702 if ( deferredRenderBlock )
1703 {
1704 DeferredRenderFragment renderFragment;
1705 renderFragment.color = textColor;
1706 if ( forceRenderAsPaths )
1707 {
1708 renderFragment.path.setFillRule( Qt::WindingFill );
1709 renderFragment.path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1710 }
1711 renderFragment.font = fragmentFont;
1712 renderFragment.point = QPointF( xOffset, yOffset );
1713 renderFragment.text = fragment.text();
1714 deferredRenderBlock->fragments.append( renderFragment );
1715 }
1716 else if ( forceRenderAsPaths )
1717 {
1718 painter->setBrush( textColor );
1719 QPainterPath path;
1720 path.setFillRule( Qt::WindingFill );
1721 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1722 painter->drawPath( path );
1723 }
1724 else
1725 {
1726 painter->setPen( textColor );
1727 painter->setFont( fragmentFont );
1728 painter->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1729 }
1730 }
1731 else if ( fragment.isImage() )
1732 {
1733 bool fitsInCache = false;
1734 const double imageWidth = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1735 const double imageHeight = metrics.fragmentFixedHeight( blockIndex, fragmentIndex, mode ) * fontScale;
1736
1737 const QImage image = QgsApplication::imageCache()->pathAsImage(
1738 fragment.characterFormat().imagePath(),
1739 QSize( static_cast< int >( std::round( imageWidth ) ), static_cast< int >( std::round( imageHeight ) ) ),
1740 false,
1741 1,
1742 fitsInCache,
1744 );
1745 const double imageBaseline = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1746 const double yOffset = imageBaseline - image.height();
1747 if ( !image.isNull() )
1748 painter->drawImage( QPointF( xOffset, yOffset ), image );
1749 }
1750
1751 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1752 fragmentIndex++;
1753 }
1754 }
1755};
1756
1757bool QgsTextRenderer::usePathsToRender( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
1758{
1759 switch ( context.textRenderFormat() )
1760 {
1762 return true;
1764 return false;
1766 {
1767 // Prefer not to use paths -- but certain conditions will require us to use them
1768 if ( format.buffer().enabled() )
1769 {
1770 // text buffer requires use of paths
1771 // TODO: this was the original cause of use switching from text to paths by default,
1772 // but that was way back in the 2.0 days and maybe the Qt issues have now been fixed?
1773 return true;
1774 }
1775
1776 // underline/overline/strikethrough looks different between path/non-path renders.
1777 // TODO: validate which is correct. For now, maintain default appearance from before this code
1778 // was introduced
1779 if ( format.font().underline() || format.font().overline() || format.font().strikeOut() || std::any_of( document.begin(), document.end(), []( const QgsTextBlock &block ) {
1780 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment &fragment ) {
1781 return fragment.characterFormat().underline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1782 || fragment.characterFormat().overline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1783 || fragment.characterFormat().strikeOut() == QgsTextCharacterFormat::BooleanValue::SetTrue;
1784 } );
1785 } ) )
1786 return true;
1787
1788 return false;
1789 }
1790 }
1792}
1793
1794bool QgsTextRenderer::usePictureToRender( const QgsRenderContext &, const QgsTextFormat &, const QgsTextDocument &document )
1795{
1796 return std::any_of( document.begin(), document.end(), []( const QgsTextBlock &block ) {
1797 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment &fragment ) { return fragment.isImage(); } );
1798 } );
1799}
1800
1801QVector< QgsTextRenderer::BlockMetrics > QgsTextRenderer::calculateBlockMetrics(
1802 const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode, double targetWidth, const Qgis::TextHorizontalAlignment hAlignment
1803)
1804{
1805 QVector< BlockMetrics > blockMetrics;
1806 blockMetrics.reserve( document.size() );
1807
1808 int blockIndex = 0;
1809 for ( const QgsTextBlock &block : document )
1810 {
1811 Qgis::TextHorizontalAlignment blockAlignment = hAlignment;
1812 if ( block.blockFormat().hasHorizontalAlignmentSet() )
1813 blockAlignment = block.blockFormat().horizontalAlignment();
1814 const bool adjustForAlignment = blockAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || document.size() > 1 );
1815
1816 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 ) || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1817
1818 BlockMetrics thisBlockMetrics;
1819 // figure x offset for horizontal alignment of multiple lines
1820 thisBlockMetrics.width = metrics.blockWidth( blockIndex );
1821
1822 if ( adjustForAlignment )
1823 {
1824 double blockWidthDiff = 0;
1825 switch ( blockAlignment )
1826 {
1828 blockWidthDiff = ( targetWidth - thisBlockMetrics.width - metrics.blockLeftMargin( blockIndex ) - metrics.blockRightMargin( blockIndex ) ) * 0.5 + metrics.blockLeftMargin( blockIndex );
1829 break;
1830
1832 blockWidthDiff = targetWidth - thisBlockMetrics.width - metrics.blockRightMargin( blockIndex );
1833 break;
1834
1836 if ( !isFinalLineInParagraph && targetWidth > thisBlockMetrics.width )
1837 {
1838 calculateExtraSpacingForLineJustification( targetWidth - thisBlockMetrics.width, block, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace );
1839 thisBlockMetrics.width = targetWidth;
1840 }
1841 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1842 break;
1843
1845 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1846 break;
1847 }
1848
1849 switch ( mode )
1850 {
1855 thisBlockMetrics.xOffset = blockWidthDiff;
1856 break;
1857
1859 {
1860 switch ( blockAlignment )
1861 {
1863 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth;
1864 break;
1865
1867 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth / 2.0;
1868 break;
1869
1872 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1873 break;
1874 }
1875 }
1876 break;
1877 }
1878 }
1879 else if ( blockAlignment == Qgis::TextHorizontalAlignment::Left || blockAlignment == Qgis::TextHorizontalAlignment::Justify )
1880 {
1881 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1882 }
1883
1884 switch ( mode )
1885 {
1889 thisBlockMetrics.backgroundWidth = targetWidth;
1890 thisBlockMetrics.backgroundXOffset = 0;
1891 break;
1894 thisBlockMetrics.backgroundWidth = thisBlockMetrics.width;
1895 thisBlockMetrics.backgroundXOffset = thisBlockMetrics.xOffset;
1896 break;
1897 }
1898
1899 blockMetrics << thisBlockMetrics;
1900 blockIndex++;
1901 }
1902 return blockMetrics;
1903}
1904
1905QBrush QgsTextRenderer::createBrushForPath( QgsRenderContext &context, const QString &path )
1906{
1907 bool fitsInCache = false;
1908 // use original image size
1909 const QSize imageSize = QgsApplication::imageCache()->originalSize( path, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1910 // TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
1911 const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
1912 const double pixelsPerMm = context.scaleFactor();
1913 const double imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
1914 const double imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
1915 QBrush res;
1916 if ( imageWidth == 0 || imageHeight == 0 )
1917 return res;
1918 const QImage image
1920 ->pathAsImage( path, QSize( static_cast< int >( std::round( imageWidth ) ), static_cast< int >( std::round( imageHeight ) ) ), false, 1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1921
1922 if ( !image.isNull() )
1923 {
1924 res.setTextureImage( image );
1925 }
1926 return res;
1927}
1928
1929void QgsTextRenderer::renderDocumentBackgrounds(
1930 QgsRenderContext &context,
1931 const QgsTextDocument &document,
1932 const QgsTextDocumentMetrics &metrics,
1933 const Component &component,
1934 const QVector< QgsTextRenderer::BlockMetrics > &blockMetrics,
1936 double verticalAlignOffset,
1937 double rotation
1938)
1939{
1940 int blockIndex = 0;
1941 context.painter()->translate( component.origin );
1942 if ( !qgsDoubleNear( rotation, 0.0 ) )
1943 context.painter()->rotate( rotation );
1944
1945 context.painter()->setPen( Qt::NoPen );
1946 context.painter()->setBrush( Qt::NoBrush );
1947 for ( const QgsTextBlock &block : document )
1948 {
1949 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1950 const double blockMaximumDescent = metrics.blockMaximumDescent( blockIndex );
1951 const double blockMaximumAscent = metrics.blockMaximumAscent( blockIndex );
1952
1953 if ( block.blockFormat().hasBackground() )
1954 {
1955 QBrush backgroundBrush = block.blockFormat().backgroundBrush();
1956 if ( !block.blockFormat().backgroundImagePath().isEmpty() )
1957 {
1958 const QBrush backgroundImageBrush = createBrushForPath( context, block.blockFormat().backgroundImagePath() );
1959 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1960 backgroundBrush = backgroundImageBrush;
1961 }
1962
1963 context.painter()->setBrush( backgroundBrush );
1964 context.painter()->drawRect(
1965 QRectF( blockMetrics[blockIndex].backgroundXOffset, baseLineOffset - blockMaximumAscent, blockMetrics[blockIndex].backgroundWidth, blockMaximumDescent + blockMaximumAscent )
1966 );
1967 }
1968
1969 double xOffset = 0;
1970 int fragmentIndex = 0;
1971
1972 for ( const QgsTextFragment &fragment : block )
1973 {
1974 const double horizontalAdvance = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
1975 const double ascent = metrics.fragmentAscent( blockIndex, fragmentIndex, mode );
1976 const double descent = metrics.fragmentDescent( blockIndex, fragmentIndex, mode );
1977
1978 if ( fragment.characterFormat().hasBackground() )
1979 {
1980 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1981
1982 QBrush backgroundBrush = fragment.characterFormat().backgroundBrush();
1983 if ( !fragment.characterFormat().backgroundImagePath().isEmpty() )
1984 {
1985 const QBrush backgroundImageBrush = createBrushForPath( context, fragment.characterFormat().backgroundImagePath() );
1986 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1987 backgroundBrush = backgroundImageBrush;
1988 }
1989
1990 context.painter()->setBrush( backgroundBrush );
1991 context.painter()->drawRect( QRectF( blockMetrics[blockIndex].xOffset + xOffset, baseLineOffset + verticalAlignOffset + yOffset - ascent, horizontalAdvance, ascent + descent ) );
1992 }
1993
1994 xOffset += horizontalAdvance;
1995 fragmentIndex++;
1996 }
1997
1998 blockIndex++;
1999 }
2000
2001 context.painter()->setBrush( Qt::NoBrush );
2002
2003 if ( !qgsDoubleNear( rotation, 0.0 ) )
2004 context.painter()->rotate( -rotation );
2005 context.painter()->translate( -component.origin );
2006}
2007
2008void QgsTextRenderer::drawTextInternalHorizontal(
2009 QgsRenderContext &context,
2010 const QgsTextFormat &format,
2011 Qgis::TextComponents components,
2013 const Component &component,
2014 const QgsTextDocument &document,
2015 const QgsTextDocumentMetrics &metrics,
2016 double fontScale,
2017 const Qgis::TextHorizontalAlignment hAlignment,
2018 Qgis::TextVerticalAlignment vAlignment,
2019 double rotation
2020)
2021{
2022 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2023
2024 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
2025
2026 double targetWidth = 0.0;
2027 switch ( mode )
2028 {
2031 targetWidth = documentSize.width();
2032 break;
2033
2037 targetWidth = component.size.width();
2038 break;
2039 }
2040
2041 double verticalAlignOffset = 0;
2042
2043 if ( mode == Qgis::TextLayoutMode::Rectangle )
2044 {
2045 const double overallHeight = documentSize.height();
2046 switch ( vAlignment )
2047 {
2049 verticalAlignOffset = metrics.blockVerticalMargin( -1 );
2050 break;
2051
2053 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5 + metrics.blockVerticalMargin( -1 );
2054 break;
2055
2057 verticalAlignOffset = ( component.size.height() - overallHeight ) + metrics.blockVerticalMargin( -1 );
2058 break;
2059 }
2060 }
2061 else if ( mode == Qgis::TextLayoutMode::Point )
2062 {
2063 verticalAlignOffset = -metrics.blockVerticalMargin( document.size() - 1 );
2064 }
2065
2066 // should we use text or paths for this render?
2067 const bool usePathsForText = usePathsToRender( context, format, document );
2068
2069 // TODO -- maybe we can avoid the nested vector? Need to confirm whether painter rotation & translation can be
2070 // done ONCE only, upfront
2071 std::unique_ptr< std::vector< DeferredRenderBlock > > deferredBlocks;
2072
2073 // Depending on format settings, we may need to render in multiple passes. Eg buffer than text, or shadow than text.
2074 // We try to avoid this if possible as it requires more work, and just do a single pass, rendering text directly as we go.
2075 // If we need to do multi-pass rendering then we'll calculate paths ONCE upfront and defer actually rendering these.
2076 const bool requiresMultiPassRendering
2077 = ( components & Qgis::TextComponent::Buffer && format.buffer().enabled() )
2079 if ( requiresMultiPassRendering )
2080 {
2081 deferredBlocks = std::make_unique< std::vector< DeferredRenderBlock > >();
2082 deferredBlocks->reserve( document.size() );
2083 }
2084
2085 if ( ( components & Qgis::TextComponent::Buffer ) || ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2086 {
2087 const QVector< BlockMetrics > blockMetrics = calculateBlockMetrics( document, metrics, mode, targetWidth, hAlignment );
2088
2089 if ( document.hasBackgrounds() )
2090 {
2091 renderDocumentBackgrounds( context, document, metrics, component, blockMetrics, mode, verticalAlignOffset, rotation );
2092 }
2093
2094 int blockIndex = 0;
2095 for ( const QgsTextBlock &block : document )
2096 {
2097 const double blockHeight = metrics.blockHeight( blockIndex );
2098
2099 DeferredRenderBlock *deferredBlock = nullptr;
2100 if ( requiresMultiPassRendering && deferredBlocks )
2101 {
2102 deferredBlocks->emplace_back( DeferredRenderBlock() );
2103 deferredBlock = &deferredBlocks->back();
2104 deferredBlock->fragments.reserve( block.size() );
2105 }
2106
2107 QgsScopedQPainterState painterState( context.painter() );
2109 context.painter()->translate( component.origin );
2110 if ( !qgsDoubleNear( rotation, 0.0 ) )
2111 context.painter()->rotate( rotation );
2112
2113 // apply to the mask painter the same transformations
2114 if ( maskPainter )
2115 {
2116 maskPainter->save();
2117 maskPainter->translate( component.origin );
2118 if ( !qgsDoubleNear( rotation, 0.0 ) )
2119 maskPainter->rotate( rotation );
2120 }
2121
2122 const BlockMetrics thisBlockMetrics = blockMetrics[blockIndex];
2123 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
2124
2125 const QPointF blockOrigin( thisBlockMetrics.xOffset, baseLineOffset + verticalAlignOffset );
2126 if ( deferredBlock )
2127 deferredBlock->origin = blockOrigin;
2128 else
2129 context.painter()->translate( blockOrigin );
2130 if ( maskPainter )
2131 maskPainter->translate( blockOrigin );
2132
2133 Component subComponent;
2134 subComponent.block = block;
2135 subComponent.blockIndex = blockIndex;
2136 subComponent.size = QSizeF( thisBlockMetrics.width, blockHeight );
2137 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
2138 subComponent.rotation = -component.rotation * 180 / M_PI;
2139 subComponent.rotationOffset = 0.0;
2140 subComponent.extraWordSpacing = thisBlockMetrics.extraWordSpace * fontScale;
2141 subComponent.extraLetterSpacing = thisBlockMetrics.extraLetterSpace * fontScale;
2142 if ( deferredBlock )
2143 deferredBlock->component = subComponent;
2144
2145 // draw the mask below the text (for preview)
2146 if ( format.mask().enabled() )
2147 {
2148 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
2149 }
2150
2151 // if we are drawing both text + buffer, we'll need a path, as we HAVE to render buffers using paths
2152 const bool needsPaths = usePathsForText
2153 || ( ( components & Qgis::TextComponent::Buffer ) && format.buffer().enabled() )
2154 || ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() );
2155
2156 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2157 if ( mode == Qgis::TextLayoutMode::Labeling )
2158 {
2159 // label size has already been calculated using any symbology reference scale factor -- we need
2160 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2161 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2162 }
2163
2164 referenceScaleOverride.reset();
2165
2166 // now render the actual text
2168 {
2169 context.painter()->setCompositionMode( format.blendMode() );
2170 }
2171
2172 // scale for any print output or image saving @ specific dpi
2173 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2174
2175 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2176 context.painter()->setPen( Qt::NoPen );
2177 context.painter()->setBrush( Qt::NoBrush );
2178
2179 renderBlockHorizontal( block, blockIndex, metrics, context, format, context.painter(), needsPaths, fontScale, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace, mode, deferredBlock );
2180
2181 if ( maskPainter )
2182 maskPainter->restore();
2183
2184 blockIndex++;
2185 }
2186 }
2187
2188 if ( deferredBlocks )
2189 {
2190 renderDeferredBlocks( context, format, components, *deferredBlocks, usePathsForText, fontScale, component, rotation );
2191 }
2192}
2193
2194void QgsTextRenderer::renderDeferredBlocks(
2195 QgsRenderContext &context,
2196 const QgsTextFormat &format,
2197 Qgis::TextComponents components,
2198 const std::vector< DeferredRenderBlock > &deferredBlocks,
2199 bool usePathsForText,
2200 double fontScale,
2201 const Component &component,
2202 double rotation
2203)
2204{
2205 if ( format.buffer().enabled() && ( components & Qgis::TextComponent::Buffer ) )
2206 {
2207 renderDeferredBuffer( context, format, components, deferredBlocks, fontScale, component, rotation );
2208 }
2209
2210 if ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2211 {
2212 renderDeferredShadowForText( context, format, deferredBlocks, fontScale, component, rotation );
2213 // TODO: there's an optimisation opportunity here -- if we are ALSO rendering the text component,
2214 // we could move the actual text rendering into renderDeferredShadowForText and use the same
2215 // QPicture as we used for the shadow. But we'd need to ensure that all the settings
2216 // which control whether text is rendered as text or paths also also considered.
2217 }
2218
2219 if ( components & Qgis::TextComponent::Text )
2220 {
2221 renderDeferredText( context, deferredBlocks, usePathsForText, fontScale, component, rotation );
2222 }
2223}
2224
2225void QgsTextRenderer::renderDeferredShadowForText(
2226 QgsRenderContext &context, const QgsTextFormat &format, const std::vector< DeferredRenderBlock > &deferredBlocks, double fontScale, const Component &component, double rotation
2227)
2228{
2229 QgsScopedQPainterState painterState( context.painter() );
2231 context.painter()->translate( component.origin );
2232 if ( !qgsDoubleNear( rotation, 0.0 ) )
2233 context.painter()->rotate( rotation );
2234
2235 context.painter()->setPen( Qt::NoPen );
2236 context.painter()->setBrush( Qt::NoBrush );
2237
2238 for ( const DeferredRenderBlock &block : deferredBlocks )
2239 {
2240 Component subComponent = block.component;
2241
2242 QPainter painter( &subComponent.picture );
2243 painter.setPen( Qt::NoPen );
2244 painter.setBrush( Qt::NoBrush );
2245 painter.scale( 1 / fontScale, 1 / fontScale );
2246
2247 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2248 {
2249 if ( !fragment.path.isEmpty() )
2250 {
2251 painter.setBrush( fragment.color );
2252 painter.drawPath( fragment.path );
2253 }
2254 else
2255 {
2256 painter.setPen( fragment.color );
2257 painter.setFont( fragment.font );
2258 painter.drawText( fragment.point, fragment.text );
2259 }
2260 }
2261 painter.end();
2262
2263 subComponent.pictureBuffer = 1.0; // no pen width to deal with, but we'll add 1 px for antialiasing
2264 subComponent.origin = QPointF( 0.0, 0.0 );
2265 const QRectF pictureBoundingRect = subComponent.picture.boundingRect();
2266 subComponent.size = pictureBoundingRect.size();
2267 subComponent.offset = QPointF( -pictureBoundingRect.left(), -pictureBoundingRect.height() - pictureBoundingRect.top() );
2268
2269 context.painter()->translate( block.origin );
2270 drawShadow( context, subComponent, format );
2271 context.painter()->translate( -block.origin );
2272 }
2273}
2274
2275void QgsTextRenderer::renderDeferredBuffer(
2276 QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, const std::vector< DeferredRenderBlock > &deferredBlocks, double fontScale, const Component &component, double rotation
2277)
2278{
2279 QgsScopedQPainterState painterState( context.painter() );
2281
2282 // 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
2283 // to generate the shadow, and then render the QPicture as the buffer on top. If not, avoid the unwanted expense of the temporary QPicture
2284 // and render directly.
2285 const bool needsShadowOnBuffer = ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer );
2286 std::unique_ptr< QPicture > bufferPicture;
2287 std::unique_ptr< QPainter > bufferPainter;
2288 QPainter *prevPainter = context.painter();
2289 if ( needsShadowOnBuffer )
2290 {
2291 bufferPicture = std::make_unique< QPicture >();
2292 bufferPainter = std::make_unique< QPainter >( bufferPicture.get() );
2293 context.setPainter( bufferPainter.get() );
2294 }
2295
2296 std::unique_ptr< QgsPaintEffect > tmpEffect;
2297 if ( format.buffer().paintEffect() && format.buffer().paintEffect()->enabled() )
2298 {
2299 tmpEffect.reset( format.buffer().paintEffect()->clone() );
2300 tmpEffect->begin( context );
2301 }
2302
2303 QColor bufferColor = format.buffer().color();
2304 bufferColor.setAlphaF( format.buffer().opacity() );
2305 QPen pen( bufferColor );
2306 const QgsTextBufferSettings &buffer = format.buffer();
2307 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
2308 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
2309 pen.setWidthF( penSize * fontScale );
2310 pen.setJoinStyle( buffer.joinStyle() );
2311 context.painter()->setPen( pen );
2312
2313 // honor pref for whether to fill buffer interior
2314 if ( !buffer.fillBufferInterior() )
2315 {
2316 bufferColor.setAlpha( 0 );
2317 }
2318 context.painter()->setBrush( bufferColor );
2319
2320 context.painter()->translate( component.origin );
2321 if ( !qgsDoubleNear( rotation, 0.0 ) )
2322 context.painter()->rotate( rotation );
2323
2325 {
2326 context.painter()->setCompositionMode( format.buffer().blendMode() );
2327 }
2328
2329 for ( const DeferredRenderBlock &block : deferredBlocks )
2330 {
2331 context.painter()->translate( block.origin );
2332 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2333 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2334 {
2335 context.painter()->drawPath( fragment.path );
2336 }
2337 context.painter()->scale( fontScale, fontScale );
2338 context.painter()->translate( -block.origin );
2339 }
2340
2341 if ( tmpEffect )
2342 {
2343 tmpEffect->end( context );
2344 }
2345
2346 if ( needsShadowOnBuffer && bufferPicture )
2347 {
2348 bufferPainter->end();
2349 bufferPainter.reset();
2350 context.setPainter( prevPainter );
2351
2352 QgsTextRenderer::Component bufferComponent = component;
2353 bufferComponent.origin = QPointF( 0.0, 0.0 );
2354 bufferComponent.picture = *bufferPicture;
2355 bufferComponent.pictureBuffer = penSize / 2.0;
2356 const QRectF bufferBoundingBox = bufferPicture->boundingRect();
2357 bufferComponent.size = bufferBoundingBox.size();
2358 bufferComponent.offset = QPointF( -bufferBoundingBox.left(), -bufferBoundingBox.height() - bufferBoundingBox.top() );
2359
2360 drawShadow( context, bufferComponent, format );
2361
2362 // also draw buffer
2364 {
2365 context.painter()->setCompositionMode( buffer.blendMode() );
2366 }
2367
2368 // scale for any print output or image saving @ specific dpi
2369 context.painter()->scale( component.dpiRatio, component.dpiRatio );
2370 QgsPainting::drawPicture( context.painter(), QPointF( 0, 0 ), *bufferPicture );
2371 }
2372}
2373
2374void QgsTextRenderer::renderDeferredText(
2375 QgsRenderContext &context, const std::vector< DeferredRenderBlock > &deferredBlocks, bool usePathsForText, double fontScale, const Component &component, double rotation
2376)
2377{
2378 QgsScopedQPainterState painterState( context.painter() );
2380 context.painter()->translate( component.origin );
2381 if ( !qgsDoubleNear( rotation, 0.0 ) )
2382 context.painter()->rotate( rotation );
2383
2384 context.painter()->setPen( Qt::NoPen );
2385 context.painter()->setBrush( Qt::NoBrush );
2386
2387 // draw the text
2388 for ( const DeferredRenderBlock &block : deferredBlocks )
2389 {
2390 context.painter()->translate( block.origin );
2391 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2392
2393 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2394 {
2395 if ( usePathsForText )
2396 {
2397 context.painter()->setBrush( fragment.color );
2398 context.painter()->drawPath( fragment.path );
2399 }
2400 else
2401 {
2402 context.painter()->setPen( fragment.color );
2403 context.painter()->setFont( fragment.font );
2404 context.painter()->drawText( fragment.point, fragment.text );
2405 }
2406 }
2407
2408 context.painter()->scale( fontScale, fontScale );
2409 context.painter()->translate( -block.origin );
2410 }
2411}
2412
2413void QgsTextRenderer::drawTextInternalVertical(
2414 QgsRenderContext &context,
2415 const QgsTextFormat &format,
2416 Qgis::TextComponents components,
2418 const QgsTextRenderer::Component &component,
2419 const QgsTextDocument &document,
2420 const QgsTextDocumentMetrics &metrics,
2421 double fontScale,
2424 double rotation
2425)
2426{
2427 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2428 const QStringList textLines = document.toPlainText();
2429
2430 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2431 if ( mode == Qgis::TextLayoutMode::Labeling )
2432 {
2433 // label size has already been calculated using any symbology reference scale factor -- we need
2434 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2435 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2436 }
2437
2438 if ( metrics.isNullFontSize() )
2439 return;
2440
2441 referenceScaleOverride.reset();
2442
2443 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
2444 const double actualTextWidth = documentSize.width();
2445 double textRectWidth = 0.0;
2446
2447 switch ( mode )
2448 {
2451 textRectWidth = actualTextWidth;
2452 break;
2453
2457 textRectWidth = component.size.width();
2458 break;
2459 }
2460
2461 int maxLineLength = 0;
2462 for ( const QString &line : std::as_const( textLines ) )
2463 {
2464 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2465 }
2466
2467 const double actualLabelHeight = documentSize.height();
2468 int blockIndex = 0;
2469
2470 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2471
2472 for ( const QgsTextBlock &block : document )
2473 {
2474 QgsScopedQPainterState painterState( context.painter() );
2476
2477 context.painter()->translate( component.origin );
2478 if ( !qgsDoubleNear( rotation, 0.0 ) )
2479 context.painter()->rotate( rotation );
2480
2481 // apply to the mask painter the same transformations
2482 if ( maskPainter )
2483 {
2484 maskPainter->save();
2485 maskPainter->translate( component.origin );
2486 if ( !qgsDoubleNear( rotation, 0.0 ) )
2487 maskPainter->rotate( rotation );
2488 }
2489
2490 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2491
2492 // figure x offset of multiple lines
2493 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2494 if ( adjustForAlignment )
2495 {
2496 double hAlignmentOffset = 0;
2497 switch ( hAlignment )
2498 {
2500 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2501 break;
2502
2504 hAlignmentOffset = textRectWidth - actualTextWidth;
2505 break;
2506
2509 break;
2510 }
2511
2512 switch ( mode )
2513 {
2518 xOffset += hAlignmentOffset;
2519 break;
2520
2522 break;
2523 }
2524 }
2525
2526 double yOffset = 0.0;
2527 switch ( mode )
2528 {
2531 {
2532 if ( rotation >= -405 && rotation < -180 )
2533 {
2534 yOffset = 0;
2535 }
2536 else if ( rotation >= 0 && rotation < 45 )
2537 {
2538 xOffset -= actualTextWidth;
2539 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2540 }
2541 }
2542 else
2543 {
2544 yOffset = -actualLabelHeight;
2545 }
2546 break;
2547
2549 yOffset = -actualLabelHeight;
2550 break;
2551
2555 yOffset = 0;
2556 break;
2557 }
2558
2559 context.painter()->translate( QPointF( xOffset, yOffset ) );
2560
2561 double currentBlockYOffset = 0;
2562 int fragmentIndex = 0;
2563 for ( const QgsTextFragment &fragment : block )
2564 {
2565 QgsScopedQPainterState fragmentPainterState( context.painter() );
2566
2567 // apply some character replacement to draw symbols in vertical presentation
2568 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2569
2570 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2571
2572 QFontMetricsF fragmentMetrics( fragmentFont );
2573
2574 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2575 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2576
2577 Component subComponent;
2578 subComponent.block = QgsTextBlock( fragment );
2579 subComponent.blockIndex = blockIndex;
2580 subComponent.firstFragmentIndex = fragmentIndex;
2581 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2582 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2583 subComponent.rotation = -component.rotation * 180 / M_PI;
2584 subComponent.rotationOffset = 0.0;
2585
2586 // draw the mask below the text (for preview)
2587 if ( format.mask().enabled() )
2588 {
2589 // WARNING: totally broken! (has been since mask was introduced)
2590#if 0
2591 QgsTextRenderer::drawMask( context, subComponent, format );
2592#endif
2593 }
2594
2595 if ( components & Qgis::TextComponent::Buffer )
2596 {
2597 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2598 }
2599 if ( ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2600 {
2601 // draw text, QPainterPath method
2602 QPainterPath path;
2603 path.setFillRule( Qt::WindingFill );
2604 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2605 double partYOffset = 0.0;
2606 for ( const QString &part : parts )
2607 {
2608 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2609 partYOffset += fragmentMetrics.ascent() / fontScale;
2610 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2611 partYOffset += letterSpacing;
2612 }
2613
2614 // store text's drawing in QPicture for drop shadow call
2615 QPicture textPict;
2616 QPainter textp;
2617 textp.begin( &textPict );
2618 textp.setPen( Qt::NoPen );
2619 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2620 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2621 textp.setBrush( textColor );
2622 textp.scale( 1 / fontScale, 1 / fontScale );
2623 textp.drawPath( path );
2624
2625 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2626 // e.g. some capitalization options, but not others
2627 //textp.setFont( tmpLyr.textFont );
2628 //textp.setPen( tmpLyr.textColor );
2629 //textp.drawText( 0, 0, component.text() );
2630 textp.end();
2631
2632 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2633 {
2634 subComponent.picture = textPict;
2635 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2636 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2637 const double prevY = subComponent.offset.y();
2638 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2639 subComponent.useOrigin = true;
2640 QgsTextRenderer::drawShadow( context, subComponent, format );
2641 subComponent.useOrigin = false;
2642 subComponent.offset = QPointF( 0, prevY );
2643 }
2644
2645 // paint the text
2647 {
2648 context.painter()->setCompositionMode( format.blendMode() );
2649 }
2650
2651 // scale for any print output or image saving @ specific dpi
2652 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2653
2654 // TODO -- this should respect the context's TextRenderFormat
2655 // draw outlined text
2656 context.painter()->translate( 0, currentBlockYOffset );
2658 context.painter()->drawPicture( 0, 0, textPict );
2659 currentBlockYOffset += partYOffset;
2660 }
2661 fragmentIndex++;
2662 }
2663
2664 if ( maskPainter )
2665 maskPainter->restore();
2666 blockIndex++;
2667 }
2668}
2669
2671{
2673 return 1.0;
2674
2675 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2676
2677 // THESE THRESHOLDS MAY NEED TWEAKING!
2678
2679 // NOLINTBEGIN(bugprone-branch-clone)
2680
2681 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2682 // 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
2683 if ( pixelSize < 50 )
2684 return 200 / pixelSize;
2685 //... 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!
2686 // 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
2687 else if ( pixelSize > 200 )
2688 return 200 / pixelSize;
2689 else
2690 return 1.0;
2691
2692 // NOLINTEND(bugprone-branch-clone)
2693}
@ ForceVector
Always force vector-based rendering, even when the result will be visually different to a raster-base...
Definition qgis.h:2899
TextAnchorPoint
Anchor point of label text.
Definition qgis.h:1475
TextLayoutMode
Text layout modes.
Definition qgis.h:3104
@ Labeling
Labeling-specific layout mode.
Definition qgis.h:3107
@ Point
Text at point of origin layout mode.
Definition qgis.h:3106
@ RectangleAscentBased
Similar to Rectangle mode, but uses ascents only when calculating font and line heights.
Definition qgis.h:3109
@ RectangleCapHeightBased
Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line ...
Definition qgis.h:3108
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:3105
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3627
TextOrientation
Text orientations.
Definition qgis.h:3089
@ Vertical
Vertically oriented text.
Definition qgis.h:3091
@ RotationBased
Horizontally or vertically oriented text based on rotation (only available for map labeling).
Definition qgis.h:3092
@ Horizontal
Horizontally oriented text.
Definition qgis.h:3090
@ Round
Use rounded joins.
Definition qgis.h:2274
@ Normal
Adjacent characters are positioned in the standard way for text in the writing system in use.
Definition qgis.h:3181
@ SubScript
Characters are placed below the base line for normal text.
Definition qgis.h:3183
@ SuperScript
Characters are placed above the base line for normal text.
Definition qgis.h:3182
@ PreferText
Render text as text objects, unless doing so results in rendering artifacts or poor quality rendering...
Definition qgis.h:3028
@ AlwaysOutlines
Always render text using path objects (AKA outlines/curves). This setting guarantees the best quality...
Definition qgis.h:3022
@ AlwaysText
Always render text as text objects. While this mode preserves text objects as text for post-processin...
Definition qgis.h:3025
RenderUnit
Rendering size units.
Definition qgis.h:5655
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size).
Definition qgis.h:5659
@ Unknown
Mixed or unknown units.
Definition qgis.h:5662
@ MapUnits
Map units.
Definition qgis.h:5657
@ Pixels
Pixels.
Definition qgis.h:5658
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2957
@ RenderBlocking
Render and load remote sources in the same thread to ensure rendering remote sources (svg and images)...
Definition qgis.h:2954
TextVerticalAlignment
Text vertical alignment.
Definition qgis.h:3164
@ Bottom
Align to bottom.
Definition qgis.h:3167
@ Top
Align to top.
Definition qgis.h:3165
@ VerticalCenter
Center align.
Definition qgis.h:3166
QFlags< TextComponent > TextComponents
Text components.
Definition qgis.h:3134
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3145
@ Justify
Justify align.
Definition qgis.h:3149
@ Center
Center align.
Definition qgis.h:3147
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3624
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
Definition qgis.h:3206
TextComponent
Text components.
Definition qgis.h:3121
@ Shadow
Drop shadow.
Definition qgis.h:3125
@ Buffer
Buffer component.
Definition qgis.h:3123
@ Text
Text component.
Definition qgis.h:3122
@ Background
Background shape.
Definition qgis.h:3124
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:8122
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7488
const char * finder(const char *name)
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30