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