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