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