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