QGIS API Documentation 3.39.0-Master (47f7b3a4989)
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
33#include <optional>
34
35#include <QTextBoundaryFinder>
36
37Q_GUI_EXPORT extern int qt_defaultDpiX();
38Q_GUI_EXPORT extern int qt_defaultDpiY();
39
40static void _fixQPictureDPI( QPainter *p )
41{
42 // QPicture makes an assumption that we drawing to it with system DPI.
43 // Then when being drawn, it scales the painter. The following call
44 // negates the effect. There is no way of setting QPicture's DPI.
45 // See QTBUG-20361
46 p->scale( static_cast< double >( qt_defaultDpiX() ) / p->device()->logicalDpiX(),
47 static_cast< double >( qt_defaultDpiY() ) / p->device()->logicalDpiY() );
48}
49
51{
52 if ( alignment & Qt::AlignLeft )
54 else if ( alignment & Qt::AlignRight )
56 else if ( alignment & Qt::AlignHCenter )
58 else if ( alignment & Qt::AlignJustify )
60
61 // not supported?
63}
64
66{
67 if ( alignment & Qt::AlignTop )
69 else if ( alignment & Qt::AlignBottom )
71 else if ( alignment & Qt::AlignVCenter )
73 //not supported
74 else if ( alignment & Qt::AlignBaseline )
76
78}
79
80int QgsTextRenderer::sizeToPixel( double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale )
81{
82 return static_cast< int >( c.convertToPainterUnits( size, unit, mapUnitScale ) + 0.5 ); //NOLINT
83}
84
85void 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,
87{
88 QgsTextFormat lFormat = _format;
89 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use format instead of tmpFormat here, it's const and potentially avoids a detach
90 lFormat.updateDataDefinedProperties( context );
91
92 // DO NOT USE _format in the following code, always use lFormat!!
93
94 QStringList textLines;
95 for ( const QString &line : text )
96 {
97 if ( flags & Qgis::TextRendererFlag::WrapLines && textRequiresWrapping( context, line, rect.width(), lFormat ) )
98 {
99 textLines.append( wrappedText( context, line, rect.width(), lFormat ) );
100 }
101 else
102 {
103 textLines.append( line );
104 }
105 }
106
107 QgsTextDocument document = lFormat.allowHtmlFormatting() ? QgsTextDocument::fromHtml( textLines ) : QgsTextDocument::fromPlainText( textLines );
108 document.applyCapitalization( lFormat.capitalization() );
109
110 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
111 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
112
113 drawDocument( rect, lFormat, document, metrics, context, alignment, vAlignment, rotation, mode, flags );
114}
115
116void 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 )
117{
118 const QgsTextFormat tmpFormat = updateShadowPosition( format );
119
120 if ( tmpFormat.background().enabled() )
121 {
122 drawPart( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, Qgis::TextComponent::Background, mode );
123 }
124
125 if ( tmpFormat.buffer().enabled() )
126 {
127 drawPart( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, Qgis::TextComponent::Buffer, mode );
128 }
129
130 drawPart( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, Qgis::TextComponent::Text, mode );
131}
132
133void QgsTextRenderer::drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &_format, bool )
134{
135 QgsTextFormat lFormat = _format;
136 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
137 lFormat.updateDataDefinedProperties( context );
138 lFormat = updateShadowPosition( lFormat );
139
140 // DO NOT USE _format in the following code, always use lFormat!!
141 QgsTextDocument document = lFormat.allowHtmlFormatting() ? QgsTextDocument::fromHtml( textLines ) : QgsTextDocument::fromPlainText( textLines );
142 document.applyCapitalization( lFormat.capitalization() );
143 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
144 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
145
146 if ( lFormat.background().enabled() )
147 {
148 drawPart( point, rotation, alignment, document, metrics, context, lFormat, Qgis::TextComponent::Background, Qgis::TextLayoutMode::Point );
149 }
150
151 if ( lFormat.buffer().enabled() )
152 {
153 drawPart( point, rotation, alignment, document, metrics, context, lFormat, Qgis::TextComponent::Buffer, Qgis::TextLayoutMode::Point );
154 }
155
156 drawPart( point, rotation, alignment, document, metrics, context, lFormat, Qgis::TextComponent::Text, Qgis::TextLayoutMode::Point );
157}
158
159void QgsTextRenderer::drawTextOnLine( const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &_format, double offsetAlongLine, double offsetFromLine )
160{
161 QgsTextFormat lFormat = _format;
162 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
163 lFormat.updateDataDefinedProperties( context );
164 lFormat = updateShadowPosition( lFormat );
165
166 // DO NOT USE _format in the following code, always use lFormat!!
167
168 // todo handle newlines??
170 document.applyCapitalization( lFormat.capitalization() );
171
172 drawDocumentOnLine( line, lFormat, document, context, offsetAlongLine, offsetFromLine );
173}
174
175void QgsTextRenderer::drawDocumentOnLine( const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine, double offsetFromLine )
176{
177 QPolygonF labelBaselineCurve = line;
178 if ( !qgsDoubleNear( offsetFromLine, 0 ) )
179 {
180 std::unique_ptr < QgsLineString > ring( QgsLineString::fromQPolygonF( line ) );
181 QgsGeos geos( ring.get() );
182 std::unique_ptr < QgsLineString > offsetCurve( dynamic_cast< QgsLineString * >( geos.offsetCurve( offsetFromLine, 4, Qgis::JoinStyle::Round, 2 ) ) );
183 if ( !offsetCurve )
184 return;
185
186#if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<11
187 if ( offsetFromLine < 0 )
188 {
189 // geos < 3.11 reverses the direction of offset curves with negative distances -- we don't want that!
190 std::unique_ptr < QgsLineString > reversed( offsetCurve->reversed() );
191 if ( !reversed )
192 return;
193
194 offsetCurve = std::move( reversed );
195 }
196#endif
197
198 labelBaselineCurve = offsetCurve->asQPolygonF();
199 }
200
201 const double fontScale = calculateScaleFactorForFormat( context, format );
202
203 const QFont baseFont = format.scaledFont( context, fontScale );
204 const double letterSpacing = baseFont.letterSpacing() / fontScale;
205 const double wordSpacing = baseFont.wordSpacing() / fontScale;
206
207 QStringList graphemes;
208 QVector< QgsTextCharacterFormat > graphemeFormats;
209 QVector< QgsTextDocument > graphemeDocuments;
210 QVector< QgsTextDocumentMetrics > graphemeMetrics;
211
212 for ( const QgsTextBlock &block : std::as_const( document ) )
213 {
214 for ( const QgsTextFragment &fragment : block )
215 {
216 const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
217 for ( const QString &grapheme : fragmentGraphemes )
218 {
219 graphemes.append( grapheme );
220 graphemeFormats.append( fragment.characterFormat() );
221
222 QgsTextDocument document;
223 document.append( QgsTextBlock( QgsTextFragment( grapheme, fragment.characterFormat() ) ) );
224
225 graphemeDocuments.append( document );
226 graphemeMetrics.append( QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale ) );
227 }
228 }
229 }
230
231 QVector< double > characterWidths( graphemes.count() );
232 QVector< double > characterHeights( graphemes.count() );
233 QVector< double > characterDescents( graphemes.count() );
234 QFont previousNonSuperSubScriptFont;
235
236 for ( int i = 0; i < graphemes.count(); i++ )
237 {
238 // reconstruct how Qt creates word spacing, then adjust per individual stored character
239 // this will allow the text renderer to create each candidate width = character width + correct spacing
240
241 double graphemeFirstCharHorizontalAdvanceWithLetterSpacing = 0;
242 double graphemeFirstCharHorizontalAdvance = 0;
243 double graphemeHorizontalAdvance = 0;
244 double characterDescent = 0;
245 double characterHeight = 0;
246 const QgsTextCharacterFormat *graphemeFormat = &graphemeFormats[i];
247
248 QFont graphemeFont = baseFont;
249 graphemeFormat->updateFontForFormat( graphemeFont, context, fontScale );
250
251 if ( i == 0 )
252 previousNonSuperSubScriptFont = graphemeFont;
253
254 if ( graphemeFormat->hasVerticalAlignmentSet() )
255 {
256 switch ( graphemeFormat->verticalAlignment() )
257 {
259 previousNonSuperSubScriptFont = graphemeFont;
260 break;
261
264 {
265 if ( graphemeFormat->fontPointSize() < 0 )
266 {
267 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
268 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
269 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
270 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
271 // which we should respect
272 graphemeFont.setPixelSize( static_cast< int >( std::round( graphemeFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
273 }
274 break;
275 }
276 }
277 }
278 else
279 {
280 previousNonSuperSubScriptFont = graphemeFont;
281 }
282
283 const QFontMetricsF graphemeFontMetrics( graphemeFont );
284 graphemeFirstCharHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ) / fontScale;
285 graphemeFirstCharHorizontalAdvanceWithLetterSpacing = graphemeFontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) / fontScale + letterSpacing;
286 graphemeHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i] ) ) / fontScale;
287 characterDescent = graphemeFontMetrics.descent() / fontScale;
288 characterHeight = graphemeFontMetrics.height() / fontScale;
289
290 qreal wordSpaceFix = qreal( 0.0 );
291 if ( graphemes[i] == QLatin1String( " " ) )
292 {
293 // word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
294 int nxt = i + 1;
295 wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != QLatin1String( " " ) ) ? wordSpacing : qreal( 0.0 );
296 }
297
298 // this workaround only works for clusters with a single character. Not sure how it should be handled
299 // with multi-character clusters.
300 if ( graphemes[i].length() == 1 &&
301 !qgsDoubleNear( graphemeFirstCharHorizontalAdvance, graphemeFirstCharHorizontalAdvanceWithLetterSpacing ) )
302 {
303 // word spacing applied when it shouldn't be
304 wordSpaceFix -= wordSpacing;
305 }
306
307 const double charWidth = graphemeHorizontalAdvance + wordSpaceFix;
308 characterWidths[i] = charWidth;
309 characterHeights[i] = characterHeight;
310 characterDescents[i] = characterDescent;
311 }
312
313 QgsPrecalculatedTextMetrics metrics( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) );
314 metrics.setGraphemeFormats( graphemeFormats );
315
316 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement = QgsTextRendererUtils::generateCurvedTextPlacement(
317 metrics, labelBaselineCurve, offsetAlongLine,
319 -1, -1,
322 );
323
324 if ( placement->graphemePlacement.empty() )
325 return;
326
327 std::vector< QgsTextRenderer::Component > components;
328 components.reserve( placement->graphemePlacement.size() );
329 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
330 {
331 QgsTextRenderer::Component component;
332 component.origin = QPointF( grapheme.x, grapheme.y );
333 component.rotation = -grapheme.angle;
334
335 QgsTextDocumentMetrics &metrics = graphemeMetrics[ grapheme.graphemeIndex ];
336 const double verticalOffset = metrics.fragmentVerticalOffset( 0, 0, Qgis::TextLayoutMode::Point );
337 if ( !qgsDoubleNear( verticalOffset, 0 ) )
338 {
339 component.origin.rx() += verticalOffset * std::cos( grapheme.angle + M_PI_2 );
340 component.origin.ry() += verticalOffset * std::sin( grapheme.angle + M_PI_2 );
341 }
342
343 components.emplace_back( component );
344 }
345
346 if ( format.background().enabled() )
347 {
348 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
349 {
350 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
351 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
352 drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Point );
353 }
354 }
355
356 if ( format.buffer().enabled() )
357 {
358 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
359 {
360 const QgsTextDocument &document = graphemeDocuments.at( grapheme.graphemeIndex );
361 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
362 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
363
364 drawTextInternal( Qgis::TextComponent::Buffer,
365 context,
366 format,
367 component,
368 document,
369 metrics,
373 }
374 }
375
376 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
377 {
378 const QgsTextDocument &document = graphemeDocuments.at( grapheme.graphemeIndex );
379 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
380 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex ];
381
382 drawTextInternal( Qgis::TextComponent::Text,
383 context,
384 format,
385 component,
386 document,
387 metrics,
391 }
392}
393
394QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format )
395{
397 return format;
398
399 QgsTextFormat tmpFormat = format;
400 if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds
401 {
403 }
404 else if ( tmpFormat.buffer().enabled() )
405 {
407 }
408 else
409 {
411 }
412 return tmpFormat;
413}
414
415void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment,
416 const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
417{
418 const QgsTextDocument document = format.allowHtmlFormatting() ? QgsTextDocument::fromHtml( textLines ) : QgsTextDocument::fromPlainText( textLines );
419 const double fontScale = calculateScaleFactorForFormat( context, format );
420 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
421
422 drawPart( rect, rotation, alignment, Qgis::TextVerticalAlignment::Top, document, metrics, context, format, part, Qgis::TextLayoutMode::Rectangle );
423}
424
425void QgsTextRenderer::drawPart( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, Qgis::TextVerticalAlignment vAlignment, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, Qgis::TextLayoutMode mode )
426{
427 if ( !context.painter() )
428 {
429 return;
430 }
431
432 Component component;
433 component.dpiRatio = 1.0;
434 component.origin = rect.topLeft();
435 component.rotation = rotation;
436 component.size = rect.size();
437 component.hAlign = alignment;
438
439 switch ( part )
440 {
442 {
443 if ( !format.background().enabled() )
444 return;
445
446 if ( !qgsDoubleNear( rotation, 0.0 ) )
447 {
448 // get rotated label's center point
449
450 double xc = rect.width() / 2.0;
451 double yc = rect.height() / 2.0;
452
453 double angle = -rotation;
454 double xd = xc * std::cos( angle ) - yc * std::sin( angle );
455 double yd = xc * std::sin( angle ) + yc * std::cos( angle );
456
457 component.center = QPointF( component.origin.x() + xd, component.origin.y() + yd );
458 }
459 else
460 {
461 component.center = rect.center();
462 }
463
464 switch ( vAlignment )
465 {
467 break;
469 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
470 break;
472 component.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
473 break;
474 }
475
476 QgsTextRenderer::drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Rectangle );
477
478 break;
479 }
480
482 {
483 if ( !format.buffer().enabled() )
484 break;
485 }
486 [[fallthrough]];
489 {
490 drawTextInternal( part, context, format, component,
491 document, metrics,
492 alignment, vAlignment, mode );
493 break;
494 }
495 }
496}
497
498void QgsTextRenderer::drawPart( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool )
499{
500 const QgsTextDocument document = format.allowHtmlFormatting() ? QgsTextDocument::fromHtml( textLines ) : QgsTextDocument::fromPlainText( textLines );
501 const double fontScale = calculateScaleFactorForFormat( context, format );
502 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
503
504 drawPart( origin, rotation, alignment, document, metrics, context, format, part, Qgis::TextLayoutMode::Point );
505}
506
507void QgsTextRenderer::drawPart( QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, Qgis::TextLayoutMode mode )
508{
509 if ( !context.painter() )
510 {
511 return;
512 }
513
514 Component component;
515 component.dpiRatio = 1.0;
516 component.origin = origin;
517 component.rotation = rotation;
518 component.hAlign = alignment;
519
520 switch ( part )
521 {
523 {
524 if ( !format.background().enabled() )
525 return;
526
527 QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
528 break;
529 }
530
532 {
533 if ( !format.buffer().enabled() )
534 break;
535 }
536 [[fallthrough]];
539 {
540 drawTextInternal( part, context, format, component,
541 document,
542 metrics,
544 mode );
545 break;
546 }
547 }
548}
549
550QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
551{
552 return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
553}
554
555double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format,
556 const QgsTextDocumentMetrics &metrics,
558{
559 QPainter *p = context.painter();
560
561 Qgis::TextOrientation orientation = format.orientation();
563 {
564 if ( component.rotation >= -315 && component.rotation < -90 )
565 {
567 }
568 else if ( component.rotation >= -90 && component.rotation < -45 )
569 {
571 }
572 else
573 {
575 }
576 }
577
578 QgsTextBufferSettings buffer = format.buffer();
579
580 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage
581 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
582 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
583
584 const double scaleFactor = calculateScaleFactorForFormat( context, format );
585
586 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
587 if ( mode == Qgis::TextLayoutMode::Labeling )
588 {
589 // label size has already been calculated using any symbology reference scale factor -- we need
590 // to temporarily remove the reference scale here or we'll be applying the scaling twice
591 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
592 }
593
594 if ( metrics.isNullFontSize() )
595 return 0;
596
597 referenceScaleOverride.reset();
598
599 QPainterPath path;
600 path.setFillRule( Qt::WindingFill );
601 double advance = 0;
602 double height = component.size.height();
603 switch ( orientation )
604 {
606 {
607 double xOffset = 0;
608 int fragmentIndex = 0;
609 for ( const QgsTextFragment &fragment : component.block )
610 {
611 QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
612
613 if ( !fragment.isWhitespace() )
614 {
615 if ( component.extraWordSpacing || component.extraLetterSpacing )
616 applyExtraSpacingForLineJustification( fragmentFont, component.extraWordSpacing, component.extraLetterSpacing );
617
618 const double yOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
619 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
620 }
621
622 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
623
624 fragmentIndex++;
625 }
626 advance = xOffset;
627 break;
628 }
629
632 {
633 double partYOffset = component.offset.y() * scaleFactor;
634
635 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
636 double partLastDescent = 0;
637
638 int fragmentIndex = 0;
639 for ( const QgsTextFragment &fragment : component.block )
640 {
641 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
642 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
643
644 const QFontMetricsF fragmentMetrics( fragmentFont );
645
646 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
647
648 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
649 for ( const QString &part : parts )
650 {
651 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
652 partYOffset += fragmentMetrics.ascent() / scaleFactor;
653 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
654 partYOffset += letterSpacing;
655 }
656 partLastDescent = fragmentMetrics.descent() / scaleFactor;
657
658 fragmentIndex++;
659 }
660 height = partYOffset + partLastDescent;
661 advance = partYOffset - component.offset.y() * scaleFactor;
662 break;
663 }
664 }
665
666 QColor bufferColor = buffer.color();
667 bufferColor.setAlphaF( buffer.opacity() );
668 QPen pen( bufferColor );
669 pen.setWidthF( penSize * scaleFactor );
670 pen.setJoinStyle( buffer.joinStyle() );
671 QColor tmpColor( bufferColor );
672 // honor pref for whether to fill buffer interior
673 if ( !buffer.fillBufferInterior() )
674 {
675 tmpColor.setAlpha( 0 );
676 }
677
678 // store buffer's drawing in QPicture for drop shadow call
679 QPicture buffPict;
680 QPainter buffp;
681 buffp.begin( &buffPict );
682 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
683 {
684 context.setPainter( &buffp );
685 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
686
687 tmpEffect->begin( context );
688 context.painter()->setPen( pen );
689 context.painter()->setBrush( tmpColor );
690 if ( scaleFactor != 1.0 )
691 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
692 context.painter()->drawPath( path );
693 if ( scaleFactor != 1.0 )
694 context.painter()->scale( scaleFactor, scaleFactor );
695 tmpEffect->end( context );
696
697 context.setPainter( p );
698 }
699 else
700 {
701 if ( scaleFactor != 1.0 )
702 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
703 buffp.setPen( pen );
704 buffp.setBrush( tmpColor );
705 buffp.drawPath( path );
706 }
707 buffp.end();
708
710 {
711 QgsTextRenderer::Component bufferComponent = component;
712 bufferComponent.origin = QPointF( 0.0, 0.0 );
713 bufferComponent.picture = buffPict;
714 bufferComponent.pictureBuffer = penSize / 2.0;
715 bufferComponent.size.setHeight( height );
716
718 {
719 bufferComponent.offset.setY( - bufferComponent.size.height() );
720 }
721 drawShadow( context, bufferComponent, format );
722 }
723
724 QgsScopedQPainterState painterState( p );
725 context.setPainterFlagsUsingContext( p );
726
727 if ( context.useAdvancedEffects() )
728 {
729 p->setCompositionMode( buffer.blendMode() );
730 }
731
732 // scale for any print output or image saving @ specific dpi
733 p->scale( component.dpiRatio, component.dpiRatio );
734 _fixQPictureDPI( p );
735 p->drawPicture( 0, 0, buffPict );
736
737 return advance / scaleFactor;
738}
739
740void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics,
742{
743 QgsTextMaskSettings mask = format.mask();
744
745 // the mask is drawn to a side painter
746 // or to the main painter for preview
747 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
748 if ( ! p )
749 return;
750
751 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage
752 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
753 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
754
755 // buffer: draw the text with a big pen
756 QPainterPath path;
757 path.setFillRule( Qt::WindingFill );
758
759 const double scaleFactor = calculateScaleFactorForFormat( context, format );
760
761 // TODO: vertical text mode was ignored when masking feature was added.
762 // Hopefully Oslandia come back and fix this? Hint hint...
763
764 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
765 if ( mode == Qgis::TextLayoutMode::Labeling )
766 {
767 // label size has already been calculated using any symbology reference scale factor -- we need
768 // to temporarily remove the reference scale here or we'll be applying the scaling twice
769 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
770 }
771
772 if ( metrics.isNullFontSize() )
773 return;
774
775 referenceScaleOverride.reset();
776
777 double xOffset = 0;
778 int fragmentIndex = 0;
779 for ( const QgsTextFragment &fragment : component.block )
780 {
781 if ( !fragment.isWhitespace() )
782 {
783 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
784
785 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
786 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
787 }
788
789 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
790 fragmentIndex++;
791 }
792
793 QColor bufferColor( Qt::gray );
794 bufferColor.setAlphaF( mask.opacity() );
795
796 QPen pen;
797 QBrush brush;
798 brush.setColor( bufferColor );
799 pen.setColor( bufferColor );
800 pen.setWidthF( penSize * scaleFactor );
801 pen.setJoinStyle( mask.joinStyle() );
802
803 QgsScopedQPainterState painterState( p );
804 context.setPainterFlagsUsingContext( p );
805
806 // scale for any print output or image saving @ specific dpi
807 p->scale( component.dpiRatio, component.dpiRatio );
808 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
809 {
810 QgsPainterSwapper swapper( context, p );
811 {
812 QgsEffectPainter effectPainter( context, mask.paintEffect() );
813 if ( scaleFactor != 1.0 )
814 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
815 context.painter()->setPen( pen );
816 context.painter()->setBrush( brush );
817 context.painter()->drawPath( path );
818 if ( scaleFactor != 1.0 )
819 context.painter()->scale( scaleFactor, scaleFactor );
820 }
821 }
822 else
823 {
824 if ( scaleFactor != 1.0 )
825 p->scale( 1 / scaleFactor, 1 / scaleFactor );
826 p->setPen( pen );
827 p->setBrush( brush );
828 p->drawPath( path );
829 if ( scaleFactor != 1.0 )
830 p->scale( scaleFactor, scaleFactor );
831
832 }
833}
834
835double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
836{
837 QgsTextDocument doc;
838 if ( !format.allowHtmlFormatting() )
839 {
840 doc = QgsTextDocument::fromPlainText( textLines );
841 }
842 else
843 {
844 doc = QgsTextDocument::fromHtml( textLines );
845 }
846 if ( doc.size() == 0 )
847 return 0;
848
849 doc.applyCapitalization( format.capitalization() );
850 return textWidth( context, format, doc );
851}
852
853double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
854{
855 //calculate max width of text lines
856 const double scaleFactor = calculateScaleFactorForFormat( context, format );
857
858 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
859
860 // width doesn't change depending on layout mode, we can use anything here
861 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
862}
863
864double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth )
865{
866 QStringList lines;
867 for ( const QString &line : textLines )
868 {
869 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
870 {
871 lines.append( wrappedText( context, line, maxLineWidth, format ) );
872 }
873 else
874 {
875 lines.append( line );
876 }
877 }
878
879 if ( !format.allowHtmlFormatting() )
880 {
881 return textHeight( context, format, QgsTextDocument::fromPlainText( lines ), mode );
882 }
883 else
884 {
885 return textHeight( context, format, QgsTextDocument::fromHtml( lines ), mode );
886 }
887}
888
889double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
890{
891 const double scaleFactor = calculateScaleFactorForFormat( context, format );
892
893 bool isNullSize = false;
894 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
895 if ( isNullSize )
896 return 0;
897
898 const QFontMetrics fm( baseFont );
899 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
900
901 if ( !includeEffects )
902 return height;
903
904 double maxExtension = 0;
905 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
906 if ( format.buffer().enabled() )
907 {
908 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
909 ? fontSize * format.buffer().size() / 100
910 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
911 }
912 if ( format.shadow().enabled() )
913 {
914 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
915 ? fontSize * format.shadow().offsetDistance() / 100
916 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
917 )
919 ? fontSize * format.shadow().blurRadius() / 100
920 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
921 );
922 }
923 if ( format.background().enabled() )
924 {
925 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
927 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
928 {
929 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
930 }
931 }
932
933 return height + maxExtension;
934}
935
936bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
937{
938 if ( qgsDoubleNear( width, 0.0 ) )
939 return false;
940
941 const QStringList multiLineSplit = text.split( '\n' );
942 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
943 return currentTextWidth > width;
944}
945
946QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
947{
948 const QStringList lines = text.split( '\n' );
949 QStringList outLines;
950 for ( const QString &line : lines )
951 {
952 if ( textRequiresWrapping( context, line, width, format ) )
953 {
954 //first step is to identify words which must be on their own line (too long to fit)
955 const QStringList words = line.split( ' ' );
956 QStringList linesToProcess;
957 QString wordsInCurrentLine;
958 for ( const QString &word : words )
959 {
960 if ( textRequiresWrapping( context, word, width, format ) )
961 {
962 //too long to fit
963 if ( !wordsInCurrentLine.isEmpty() )
964 linesToProcess << wordsInCurrentLine;
965 wordsInCurrentLine.clear();
966 linesToProcess << word;
967 }
968 else
969 {
970 if ( !wordsInCurrentLine.isEmpty() )
971 wordsInCurrentLine.append( ' ' );
972 wordsInCurrentLine.append( word );
973 }
974 }
975 if ( !wordsInCurrentLine.isEmpty() )
976 linesToProcess << wordsInCurrentLine;
977
978 for ( const QString &line : std::as_const( linesToProcess ) )
979 {
980 QString remainingText = line;
981 int lastPos = remainingText.lastIndexOf( ' ' );
982 while ( lastPos > -1 )
983 {
984 //check if remaining text is short enough to go in one line
985 if ( !textRequiresWrapping( context, remainingText, width, format ) )
986 {
987 break;
988 }
989
990 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
991 {
992 outLines << remainingText.left( lastPos );
993 remainingText = remainingText.mid( lastPos + 1 );
994 lastPos = 0;
995 }
996 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
997 }
998 outLines << remainingText;
999 }
1000 }
1001 else
1002 {
1003 outLines << line;
1004 }
1005 }
1006
1007 return outLines;
1008}
1009
1010double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
1011{
1012 QgsTextDocument document = doc;
1013 document.applyCapitalization( format.capitalization() );
1014
1015 //calculate max height of text lines
1016 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1017
1018 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
1019 if ( metrics.isNullFontSize() )
1020 return 0;
1021
1022 return metrics.documentSize( mode, format.orientation() ).height();
1023}
1024
1025void QgsTextRenderer::drawBackground( QgsRenderContext &context, QgsTextRenderer::Component component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
1026{
1027 QgsTextBackgroundSettings background = format.background();
1028
1029 QPainter *prevP = context.painter();
1030 QPainter *p = context.painter();
1031 std::unique_ptr< QgsPaintEffect > tmpEffect;
1032 if ( background.paintEffect() && background.paintEffect()->enabled() )
1033 {
1034 tmpEffect.reset( background.paintEffect()->clone() );
1035 tmpEffect->begin( context );
1036 p = context.painter();
1037 }
1038
1039 //QgsDebugMsgLevel( QStringLiteral( "Background label rotation: %1" ).arg( component.rotation() ), 4 );
1040
1041 // shared calculations between shapes and SVG
1042
1043 // configure angles, set component rotation and rotationOffset
1044 const double originAdjustRotationRadians = -component.rotation;
1046 {
1047 component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
1048 component.rotationOffset =
1049 background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
1050 }
1051 else // RotationFixed
1052 {
1053 component.rotation = 0.0; // don't use label's rotation
1054 component.rotationOffset = background.rotation();
1055 }
1056
1057 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1058
1059 if ( mode != Qgis::TextLayoutMode::Labeling )
1060 {
1061 // need to calculate size of text
1062 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1063 double width = documentSize.width();
1064 double height = documentSize.height();
1065
1066 switch ( mode )
1067 {
1071 switch ( component.hAlign )
1072 {
1075 component.center = QPointF( component.origin.x() + width / 2.0,
1076 component.origin.y() + height / 2.0 );
1077 break;
1078
1080 component.center = QPointF( component.origin.x() + component.size.width() / 2.0,
1081 component.origin.y() + height / 2.0 );
1082 break;
1083
1085 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0,
1086 component.origin.y() + height / 2.0 );
1087 break;
1088 }
1089 break;
1090
1092 {
1093 bool isNullSize = false;
1094 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1095 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1096 switch ( component.hAlign )
1097 {
1100 component.center = QPointF( component.origin.x() + width / 2.0,
1101 component.origin.y() - height / 2.0 + originAdjust );
1102 break;
1103
1105 component.center = QPointF( component.origin.x(),
1106 component.origin.y() - height / 2.0 + originAdjust );
1107 break;
1108
1110 component.center = QPointF( component.origin.x() - width / 2.0,
1111 component.origin.y() - height / 2.0 + originAdjust );
1112 break;
1113 }
1114
1115 // apply rotation to center point
1116 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1117 {
1118 const double dx = component.center.x() - component.origin.x();
1119 const double dy = component.center.y() - component.origin.y();
1120 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1121 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1122 }
1123 break;
1124 }
1125
1127 break;
1128 }
1129
1131 component.size = QSizeF( width, height );
1132 }
1133
1134 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1135
1136 switch ( background.type() )
1137 {
1140 {
1141 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1142
1143 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1144 return;
1145
1146 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1147 return;
1148
1149 double sizeOut = 0.0;
1150 {
1151 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1152
1153 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1154 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1155 {
1156 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1157 }
1158 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1159 {
1160 sizeOut = std::max( component.size.width(), component.size.height() );
1161 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1162
1163 // add buffer
1164 sizeOut += bufferSize * 2;
1165 }
1166 }
1167
1168 // don't bother rendering symbols smaller than 1x1 pixels in size
1169 // TODO: add option to not show any svgs under/over a certain size
1170 if ( sizeOut < 1.0 )
1171 return;
1172
1173 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1174 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1175 {
1176 QVariantMap map; // for SVG symbology marker
1177 map[QStringLiteral( "name" )] = background.svgFile().trimmed();
1178 map[QStringLiteral( "size" )] = QString::number( sizeOut );
1179 map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1180 map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter
1181
1182 // offset is handled by this local painter
1183 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1184 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1185 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1186 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1187
1188 map[QStringLiteral( "fill" )] = background.fillColor().name();
1189 map[QStringLiteral( "outline" )] = background.strokeColor().name();
1190 map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() );
1191 map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1192
1194 {
1195 QgsTextShadowSettings shadow = format.shadow();
1196 // configure SVG shadow specs
1197 QVariantMap shdwmap( map );
1198 shdwmap[QStringLiteral( "fill" )] = shadow.color().name();
1199 shdwmap[QStringLiteral( "outline" )] = shadow.color().name();
1200 shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut );
1201
1202 // store SVG's drawing in QPicture for drop shadow call
1203 QPicture svgPict;
1204 QPainter svgp;
1205 svgp.begin( &svgPict );
1206
1207 // draw shadow symbol
1208
1209 // clone current render context map unit/mm conversion factors, but not
1210 // other map canvas parameters, then substitute this painter for use in symbology painting
1211 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1212 // but will be created relative to the SVG's computed size, not the current map canvas
1213 QgsRenderContext shdwContext;
1214 shdwContext.setMapToPixel( context.mapToPixel() );
1215 shdwContext.setScaleFactor( context.scaleFactor() );
1216 shdwContext.setPainter( &svgp );
1217
1218 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1219 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1220 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1221
1222 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1223 svgp.end();
1224
1225 component.picture = svgPict;
1226 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1227 component.pictureBuffer = 0.0;
1228
1229 component.size = QSizeF( sizeOut, sizeOut );
1230 component.offset = QPointF( 0.0, 0.0 );
1231
1232 // rotate about origin center of SVG
1233 QgsScopedQPainterState painterState( p );
1234 context.setPainterFlagsUsingContext( p );
1235
1236 p->translate( component.center.x(), component.center.y() );
1237 p->rotate( component.rotation );
1238 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1239 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1240 p->translate( QPointF( xoff, yoff ) );
1241 p->rotate( component.rotationOffset );
1242 p->translate( -sizeOut / 2, sizeOut / 2 );
1243
1244 drawShadow( context, component, format );
1245 }
1246 renderedSymbol.reset( );
1247
1249 renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) );
1250 }
1251 else
1252 {
1253 renderedSymbol.reset( background.markerSymbol()->clone() );
1254 renderedSymbol->setSize( sizeOut );
1255 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1256 }
1257
1258 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1259
1260 // draw the actual symbol
1261 QgsScopedQPainterState painterState( p );
1262 context.setPainterFlagsUsingContext( p );
1263
1264 if ( context.useAdvancedEffects() )
1265 {
1266 p->setCompositionMode( background.blendMode() );
1267 }
1268 p->translate( component.center.x(), component.center.y() );
1269 p->rotate( component.rotation );
1270 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1271 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1272 p->translate( QPointF( xoff, yoff ) );
1273 p->rotate( component.rotationOffset );
1274
1275 const QgsFeature f = context.expressionContext().feature();
1276 renderedSymbol->startRender( context, context.expressionContext().fields() );
1277 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1278 renderedSymbol->stopRender( context );
1279 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1280
1281 break;
1282 }
1283
1288 {
1289 double w = component.size.width();
1290 double h = component.size.height();
1291
1292 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1293 {
1294 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1295 background.sizeMapUnitScale() );
1296 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1297 background.sizeMapUnitScale() );
1298 }
1299 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1300 {
1301 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1302 {
1303 if ( w > h )
1304 h = w;
1305 else if ( h > w )
1306 w = h;
1307 }
1308 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1309 {
1310 // start with label bound by circle
1311 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1312 w = h;
1313 }
1314 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1315 {
1316 // start with label bound by ellipse
1317 h = h * M_SQRT1_2 * 2;
1318 w = w * M_SQRT1_2 * 2;
1319 }
1320
1321 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1322 background.sizeMapUnitScale() );
1323 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1324 background.sizeMapUnitScale() );
1325
1326 w += bufferWidth * 2;
1327 h += bufferHeight * 2;
1328 }
1329
1330 // offsets match those of symbology: -x = left, -y = up
1331 QRectF rect( -w / 2.0, - h / 2.0, w, h );
1332
1333 if ( rect.isNull() )
1334 return;
1335
1336 QgsScopedQPainterState painterState( p );
1337 context.setPainterFlagsUsingContext( p );
1338
1339 p->translate( QPointF( component.center.x(), component.center.y() ) );
1340 p->rotate( component.rotation );
1341 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1342 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1343 p->translate( QPointF( xoff, yoff ) );
1344 p->rotate( component.rotationOffset );
1345
1346 QPainterPath path;
1347
1348 // Paths with curves must be enlarged before conversion to QPolygonF, or
1349 // the curves are approximated too much and appear jaggy
1350 QTransform t = QTransform::fromScale( 10, 10 );
1351 // inverse transform used to scale created polygons back to expected size
1352 QTransform ti = t.inverted();
1353
1355 || background.type() == QgsTextBackgroundSettings::ShapeSquare )
1356 {
1357 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1358 {
1359 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1360 }
1361 else
1362 {
1363 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1364 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1365 path.addRoundedRect( rect, xRadius, yRadius );
1366 }
1367 }
1368 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse
1369 || background.type() == QgsTextBackgroundSettings::ShapeCircle )
1370 {
1371 path.addEllipse( rect );
1372 }
1373 QPolygonF tempPolygon = path.toFillPolygon( t );
1374 QPolygonF polygon = ti.map( tempPolygon );
1375 QPicture shapePict;
1376 QPainter *oldp = context.painter();
1377 QPainter shapep;
1378
1379 shapep.begin( &shapePict );
1380 context.setPainter( &shapep );
1381
1382 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1383 renderedSymbol.reset( background.fillSymbol()->clone() );
1384 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1385
1386 const QgsFeature f = context.expressionContext().feature();
1387 renderedSymbol->startRender( context, context.expressionContext().fields() );
1388 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1389 renderedSymbol->stopRender( context );
1390
1391 shapep.end();
1392 context.setPainter( oldp );
1393
1395 {
1396 component.picture = shapePict;
1397 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1398
1399 component.size = rect.size();
1400 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1401 drawShadow( context, component, format );
1402 }
1403
1404 if ( context.useAdvancedEffects() )
1405 {
1406 p->setCompositionMode( background.blendMode() );
1407 }
1408
1409 // scale for any print output or image saving @ specific dpi
1410 p->scale( component.dpiRatio, component.dpiRatio );
1411 _fixQPictureDPI( p );
1412 p->drawPicture( 0, 0, shapePict );
1413 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1414 break;
1415 }
1416 }
1417
1418 if ( tmpEffect )
1419 {
1420 tmpEffect->end( context );
1421 context.setPainter( prevP );
1422 }
1423}
1424
1425void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1426{
1427 QgsTextShadowSettings shadow = format.shadow();
1428
1429 // incoming component sizes should be multiplied by rasterCompressFactor, as
1430 // this allows shadows to be created at paint device dpi (e.g. high resolution),
1431 // then scale device painter by 1.0 / rasterCompressFactor for output
1432
1433 QPainter *p = context.painter();
1434 const double componentWidth = component.size.width();
1435 const double componentHeight = component.size.height();
1436 double xOffset = component.offset.x(), yOffset = component.offset.y();
1437 double pictbuffer = component.pictureBuffer;
1438
1439 // generate pixmap representation of label component drawing
1440 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1441
1442 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1443 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage
1444 ? fontSize * shadow.blurRadius() / 100
1445 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1446 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1447 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1448
1449 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1450 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1451 double blurBufferClippingScale = 3.75;
1452 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1453
1454 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1455 componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1456 QImage::Format_ARGB32_Premultiplied );
1457
1458 // TODO: add labeling gui option to not show any shadows under/over a certain size
1459 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1460 int minBlurImgSize = 1;
1461 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1462 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1463 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1464 int maxBlurImgSize = 40000;
1465 if ( blurImg.isNull()
1466 || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize )
1467 || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1468 return;
1469
1470 blurImg.fill( QColor( Qt::transparent ).rgba() );
1471 QPainter pictp;
1472 if ( !pictp.begin( &blurImg ) )
1473 return;
1474 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1475 QPointF imgOffset( blurbuffer + pictbuffer + xOffset,
1476 blurbuffer + pictbuffer + componentHeight + yOffset );
1477
1478 pictp.drawPicture( imgOffset,
1479 component.picture );
1480
1481 // overlay shadow color
1482 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1483 pictp.fillRect( blurImg.rect(), shadow.color() );
1484 pictp.end();
1485
1486 // blur the QImage in-place
1487 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1488 {
1489 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1490 }
1491
1492#if 0
1493 // debug rect for QImage shadow registration and clipping visualization
1494 QPainter picti;
1495 picti.begin( &blurImg );
1496 picti.setBrush( Qt::Dense7Pattern );
1497 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1498 imgPen.setWidth( 1 );
1499 picti.setPen( imgPen );
1500 picti.setOpacity( 0.1 );
1501 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1502 picti.end();
1503#endif
1504
1505 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage
1506 ? fontSize * shadow.offsetDistance() / 100
1507 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1508 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1509 if ( shadow.offsetGlobal() )
1510 {
1511 // TODO: check for differences in rotation origin and cw/ccw direction,
1512 // when this shadow function is used for something other than labels
1513
1514 // it's 0-->cw-->360 for labels
1515 //QgsDebugMsgLevel( QStringLiteral( "Shadow aggregated label rotation (degrees): %1" ).arg( component.rotation() + component.rotationOffset() ), 4 );
1516 angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1517 }
1518
1519 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ),
1520 -offsetDist * std::sin( angleRad + M_PI_2 ) );
1521
1522 p->save();
1523 context.setPainterFlagsUsingContext( p );
1524 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1525 // corresponding flag in the render context instead...
1526 p->setRenderHint( QPainter::SmoothPixmapTransform );
1527 if ( context.useAdvancedEffects() )
1528 {
1529 p->setCompositionMode( shadow.blendMode() );
1530 }
1531 p->setOpacity( shadow.opacity() );
1532
1533 double scale = shadow.scale() / 100.0;
1534 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1535 p->scale( scale, scale );
1536 if ( component.useOrigin )
1537 {
1538 p->translate( component.origin.x(), component.origin.y() );
1539 }
1540 p->translate( transPt );
1541 p->translate( -imgOffset.x(),
1542 -imgOffset.y() );
1543 p->drawImage( 0, 0, blurImg );
1544 p->restore();
1545
1546 // debug rects
1547#if 0
1548 // draw debug rect for QImage painting registration
1549 p->save();
1550 p->setBrush( Qt::NoBrush );
1551 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1552 imgPen.setWidth( 2 );
1553 imgPen.setStyle( Qt::DashLine );
1554 p->setPen( imgPen );
1555 p->scale( scale, scale );
1556 if ( component.useOrigin() )
1557 {
1558 p->translate( component.origin().x(), component.origin().y() );
1559 }
1560 p->translate( transPt );
1561 p->translate( -imgOffset.x(),
1562 -imgOffset.y() );
1563 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1564 p->restore();
1565
1566 // draw debug rect for passed in component dimensions
1567 p->save();
1568 p->setBrush( Qt::NoBrush );
1569 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1570 componentRectPen.setWidth( 1 );
1571 if ( component.useOrigin() )
1572 {
1573 p->translate( component.origin().x(), component.origin().y() );
1574 }
1575 p->setPen( componentRectPen );
1576 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1577 p->restore();
1578#endif
1579}
1580
1581
1582void QgsTextRenderer::drawTextInternal( Qgis::TextComponent drawType,
1583 QgsRenderContext &context,
1584 const QgsTextFormat &format,
1585 const Component &component,
1586 const QgsTextDocument &document,
1587 const QgsTextDocumentMetrics &metrics,
1589{
1590 if ( !context.painter() )
1591 {
1592 return;
1593 }
1594
1595 const double fontScale = calculateScaleFactorForFormat( context, format );
1596
1597 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1598 if ( mode == Qgis::TextLayoutMode::Labeling )
1599 {
1600 // label size has already been calculated using any symbology reference scale factor -- we need
1601 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1602 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1603 }
1604
1605 if ( metrics.isNullFontSize() )
1606 return;
1607
1608 referenceScaleOverride.reset();
1609
1610 double rotation = 0;
1611 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1612 switch ( orientation )
1613 {
1615 {
1616 drawTextInternalHorizontal( context, format, drawType, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1617 break;
1618 }
1619
1622 {
1623 drawTextInternalVertical( context, format, drawType, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1624 break;
1625 }
1626 }
1627}
1628
1629Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1630{
1631 rotation = -component.rotation * 180 / M_PI;
1632
1633 switch ( format.orientation() )
1634 {
1636 {
1637 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1638 if ( rotation >= -315 && rotation < -90 )
1639 {
1640 rotation -= 90;
1642 }
1643 else if ( rotation >= -90 && rotation < -45 )
1644 {
1645 rotation += 90;
1647 }
1648
1650 }
1651
1654 return format.orientation();
1655 }
1657}
1658
1659void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1660{
1661 const QString blockText = block.toPlainText();
1662 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1663 finder.toStart();
1664 int wordBoundaries = 0;
1665 while ( finder.toNextBoundary() != -1 )
1666 {
1667 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1668 wordBoundaries++;
1669 }
1670
1671 if ( wordBoundaries > 0 )
1672 {
1673 // word boundaries found => justify by padding word spacing
1674 extraWordSpace = spaceToDistribute / wordBoundaries;
1675 }
1676 else
1677 {
1678 // no word boundaries found => justify by letter spacing
1679 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1680 finder.toStart();
1681
1682 int graphemeBoundaries = 0;
1683 while ( finder.toNextBoundary() != -1 )
1684 {
1685 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1686 graphemeBoundaries++;
1687 }
1688
1689 if ( graphemeBoundaries > 0 )
1690 {
1691 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1692 }
1693 }
1694}
1695
1696void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1697{
1698 const double prevWordSpace = font.wordSpacing();
1699 font.setWordSpacing( prevWordSpace + extraWordSpace );
1700 const double prevLetterSpace = font.letterSpacing();
1701 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1702}
1703
1704void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent drawType, Qgis::TextLayoutMode mode, const Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, Qgis::TextHorizontalAlignment hAlignment,
1705 Qgis::TextVerticalAlignment vAlignment, double rotation )
1706{
1707 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1708 const QStringList textLines = document.toPlainText();
1709
1710 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
1711
1712 double labelWidest = 0.0;
1713 switch ( mode )
1714 {
1717 labelWidest = documentSize.width();
1718 break;
1719
1723 labelWidest = component.size.width();
1724 break;
1725 }
1726
1727 double verticalAlignOffset = 0;
1728
1729 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
1730
1732 {
1733 const double overallHeight = documentSize.height();
1734 switch ( vAlignment )
1735 {
1737 break;
1738
1740 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5;
1741 break;
1742
1744 verticalAlignOffset = ( component.size.height() - overallHeight );
1745 break;
1746 }
1747 }
1748
1749 int blockIndex = 0;
1750 for ( const QgsTextBlock &block : document )
1751 {
1752 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 )
1753 || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1754
1755 const double blockHeight = metrics.blockHeight( blockIndex );
1756
1757 QgsScopedQPainterState painterState( context.painter() );
1759 context.painter()->translate( component.origin );
1760 if ( !qgsDoubleNear( rotation, 0.0 ) )
1761 context.painter()->rotate( rotation );
1762
1763 // apply to the mask painter the same transformations
1764 if ( maskPainter )
1765 {
1766 maskPainter->save();
1767 maskPainter->translate( component.origin );
1768 if ( !qgsDoubleNear( rotation, 0.0 ) )
1769 maskPainter->rotate( rotation );
1770 }
1771
1772 // figure x offset for horizontal alignment of multiple lines
1773 double xMultiLineOffset = 0.0;
1774 double blockWidth = metrics.blockWidth( blockIndex );
1775 double extraWordSpace = 0;
1776 double extraLetterSpace = 0;
1777 if ( adjustForAlignment )
1778 {
1779 double labelWidthDiff = 0;
1780 switch ( hAlignment )
1781 {
1783 labelWidthDiff = ( labelWidest - blockWidth ) * 0.5;
1784 break;
1785
1787 labelWidthDiff = labelWidest - blockWidth;
1788 break;
1789
1791 if ( !isFinalLineInParagraph && labelWidest > blockWidth )
1792 {
1793 calculateExtraSpacingForLineJustification( labelWidest - blockWidth, block, extraWordSpace, extraLetterSpace );
1794 blockWidth = labelWidest;
1795 }
1796 break;
1797
1799 break;
1800 }
1801
1802 switch ( mode )
1803 {
1808 xMultiLineOffset = labelWidthDiff;
1809 break;
1810
1812 {
1813 switch ( hAlignment )
1814 {
1816 xMultiLineOffset = labelWidthDiff - labelWidest;
1817 break;
1818
1820 xMultiLineOffset = labelWidthDiff - labelWidest / 2.0;
1821 break;
1822
1825 break;
1826 }
1827 }
1828 break;
1829 }
1830 }
1831
1832 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1833
1834 context.painter()->translate( QPointF( xMultiLineOffset, baseLineOffset + verticalAlignOffset ) );
1835 if ( maskPainter )
1836 maskPainter->translate( QPointF( xMultiLineOffset, baseLineOffset + verticalAlignOffset ) );
1837
1838 Component subComponent;
1839 subComponent.block = block;
1840 subComponent.blockIndex = blockIndex;
1841 subComponent.size = QSizeF( blockWidth, blockHeight );
1842 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
1843 subComponent.rotation = -component.rotation * 180 / M_PI;
1844 subComponent.rotationOffset = 0.0;
1845 subComponent.extraWordSpacing = extraWordSpace * fontScale;
1846 subComponent.extraLetterSpacing = extraLetterSpace * fontScale;
1847
1848 // draw the mask below the text (for preview)
1849 if ( format.mask().enabled() )
1850 {
1851 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
1852 }
1853
1854 if ( drawType == Qgis::TextComponent::Buffer )
1855 {
1856 QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
1857 }
1858 else
1859 {
1860 // store text's drawing in QPicture for drop shadow call
1861 QPicture textPict;
1862 QPainter textp;
1863 textp.begin( &textPict );
1864 textp.setPen( Qt::NoPen );
1865
1866 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1867 if ( mode == Qgis::TextLayoutMode::Labeling )
1868 {
1869 // label size has already been calculated using any symbology reference scale factor -- we need
1870 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1871 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1872 }
1873
1874 referenceScaleOverride.reset();
1875
1876 if ( !metrics.isNullFontSize() )
1877 {
1878 textp.scale( 1 / fontScale, 1 / fontScale );
1879
1880 double xOffset = 0;
1881 int fragmentIndex = 0;
1882 for ( const QgsTextFragment &fragment : block )
1883 {
1884 // draw text, QPainterPath method
1885 if ( !fragment.isWhitespace() )
1886 {
1887 QPainterPath path;
1888 path.setFillRule( Qt::WindingFill );
1889
1890 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1891
1892 if ( extraWordSpace || extraLetterSpace )
1893 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1894
1895 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1896
1897 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1898
1899 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1900 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1901 textp.setBrush( textColor );
1902 textp.drawPath( path );
1903 }
1904
1905 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1906 fragmentIndex ++;
1907 }
1908 textp.end();
1909 }
1910
1911 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
1912 {
1913 subComponent.picture = textPict;
1914 subComponent.pictureBuffer = 0.0; // no pen width to deal with
1915 subComponent.origin = QPointF( 0.0, 0.0 );
1916
1917 QgsTextRenderer::drawShadow( context, subComponent, format );
1918 }
1919
1920 // paint the text
1921 if ( context.useAdvancedEffects() )
1922 {
1923 context.painter()->setCompositionMode( format.blendMode() );
1924 }
1925
1926 // scale for any print output or image saving @ specific dpi
1927 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
1928
1929 switch ( context.textRenderFormat() )
1930 {
1932 {
1933 // draw outlined text
1934 _fixQPictureDPI( context.painter() );
1935 context.painter()->drawPicture( 0, 0, textPict );
1936 break;
1937 }
1938
1940 {
1941 double xOffset = 0;
1942 int fragmentIndex = 0;
1943 for ( const QgsTextFragment &fragment : block )
1944 {
1945 if ( !fragment.isWhitespace() )
1946 {
1947 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1948
1949 if ( extraWordSpace || extraLetterSpace )
1950 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1951
1952 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1953
1954 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1955 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1956
1957 context.painter()->setPen( textColor );
1958 context.painter()->setFont( fragmentFont );
1959 context.painter()->setRenderHint( QPainter::TextAntialiasing );
1960
1961 context.painter()->scale( 1 / fontScale, 1 / fontScale );
1962 context.painter()->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1963 context.painter()->scale( fontScale, fontScale );
1964 }
1965
1966 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
1967 fragmentIndex++;
1968 }
1969 }
1970 }
1971 }
1972 if ( maskPainter )
1973 maskPainter->restore();
1974
1975 blockIndex++;
1976 }
1977}
1978
1979void QgsTextRenderer::drawTextInternalVertical( QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent drawType, Qgis::TextLayoutMode mode, const QgsTextRenderer::Component &component, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, double fontScale, Qgis::TextHorizontalAlignment hAlignment, Qgis::TextVerticalAlignment, double rotation )
1980{
1981 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1982 const QStringList textLines = document.toPlainText();
1983
1984 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1985 if ( mode == Qgis::TextLayoutMode::Labeling )
1986 {
1987 // label size has already been calculated using any symbology reference scale factor -- we need
1988 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1989 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1990 }
1991
1992 if ( metrics.isNullFontSize() )
1993 return;
1994
1995 referenceScaleOverride.reset();
1996
1997 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
1998 const double actualTextWidth = documentSize.width();
1999 double textRectWidth = 0.0;
2000
2001 switch ( mode )
2002 {
2005 textRectWidth = actualTextWidth;
2006 break;
2007
2011 textRectWidth = component.size.width();
2012 break;
2013 }
2014
2015 int maxLineLength = 0;
2016 for ( const QString &line : std::as_const( textLines ) )
2017 {
2018 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2019 }
2020
2021 const double actualLabelHeight = documentSize.height();
2022 int blockIndex = 0;
2023
2024 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2025
2026 for ( const QgsTextBlock &block : document )
2027 {
2028 QgsScopedQPainterState painterState( context.painter() );
2030
2031 context.painter()->translate( component.origin );
2032 if ( !qgsDoubleNear( rotation, 0.0 ) )
2033 context.painter()->rotate( rotation );
2034
2035 // apply to the mask painter the same transformations
2036 if ( maskPainter )
2037 {
2038 maskPainter->save();
2039 maskPainter->translate( component.origin );
2040 if ( !qgsDoubleNear( rotation, 0.0 ) )
2041 maskPainter->rotate( rotation );
2042 }
2043
2044 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2045
2046 // figure x offset of multiple lines
2047 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2048 if ( adjustForAlignment )
2049 {
2050 double hAlignmentOffset = 0;
2051 switch ( hAlignment )
2052 {
2054 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2055 break;
2056
2058 hAlignmentOffset = textRectWidth - actualTextWidth;
2059 break;
2060
2063 break;
2064 }
2065
2066 switch ( mode )
2067 {
2072 xOffset += hAlignmentOffset;
2073 break;
2074
2076 break;
2077 }
2078 }
2079
2080 double yOffset = 0.0;
2081 switch ( mode )
2082 {
2085 {
2086 if ( rotation >= -405 && rotation < -180 )
2087 {
2088 yOffset = 0;
2089 }
2090 else if ( rotation >= 0 && rotation < 45 )
2091 {
2092 xOffset -= actualTextWidth;
2093 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2094 }
2095 }
2096 else
2097 {
2098 yOffset = -actualLabelHeight;
2099 }
2100 break;
2101
2103 yOffset = -actualLabelHeight;
2104 break;
2105
2109 yOffset = 0;
2110 break;
2111 }
2112
2113 context.painter()->translate( QPointF( xOffset, yOffset ) );
2114
2115 double currentBlockYOffset = 0;
2116 int fragmentIndex = 0;
2117 for ( const QgsTextFragment &fragment : block )
2118 {
2119 QgsScopedQPainterState fragmentPainterState( context.painter() );
2120
2121 // apply some character replacement to draw symbols in vertical presentation
2122 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2123
2124 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2125
2126 QFontMetricsF fragmentMetrics( fragmentFont );
2127
2128 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2129 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2130
2131 Component subComponent;
2132 subComponent.block = QgsTextBlock( fragment );
2133 subComponent.blockIndex = blockIndex;
2134 subComponent.firstFragmentIndex = fragmentIndex;
2135 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2136 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2137 subComponent.rotation = -component.rotation * 180 / M_PI;
2138 subComponent.rotationOffset = 0.0;
2139
2140 // draw the mask below the text (for preview)
2141 if ( format.mask().enabled() )
2142 {
2143 // WARNING: totally broken! (has been since mask was introduced)
2144#if 0
2145 QgsTextRenderer::drawMask( context, subComponent, format );
2146#endif
2147 }
2148
2149 if ( drawType == Qgis::TextComponent::Buffer )
2150 {
2151 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2152 }
2153 else
2154 {
2155 // draw text, QPainterPath method
2156 QPainterPath path;
2157 path.setFillRule( Qt::WindingFill );
2158 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2159 double partYOffset = 0.0;
2160 for ( const QString &part : parts )
2161 {
2162 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2163 partYOffset += fragmentMetrics.ascent() / fontScale;
2164 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2165 partYOffset += letterSpacing;
2166 }
2167
2168 // store text's drawing in QPicture for drop shadow call
2169 QPicture textPict;
2170 QPainter textp;
2171 textp.begin( &textPict );
2172 textp.setPen( Qt::NoPen );
2173 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2174 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2175 textp.setBrush( textColor );
2176 textp.scale( 1 / fontScale, 1 / fontScale );
2177 textp.drawPath( path );
2178
2179 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2180 // e.g. some capitalization options, but not others
2181 //textp.setFont( tmpLyr.textFont );
2182 //textp.setPen( tmpLyr.textColor );
2183 //textp.drawText( 0, 0, component.text() );
2184 textp.end();
2185
2186 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2187 {
2188 subComponent.picture = textPict;
2189 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2190 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2191 const double prevY = subComponent.offset.y();
2192 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2193 subComponent.useOrigin = true;
2194 QgsTextRenderer::drawShadow( context, subComponent, format );
2195 subComponent.useOrigin = false;
2196 subComponent.offset = QPointF( 0, prevY );
2197 }
2198
2199 // paint the text
2200 if ( context.useAdvancedEffects() )
2201 {
2202 context.painter()->setCompositionMode( format.blendMode() );
2203 }
2204
2205 // scale for any print output or image saving @ specific dpi
2206 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2207
2208 switch ( context.textRenderFormat() )
2209 {
2211 {
2212 // draw outlined text
2213 context.painter()->translate( 0, currentBlockYOffset );
2214 _fixQPictureDPI( context.painter() );
2215 context.painter()->drawPicture( 0, 0, textPict );
2216 currentBlockYOffset += partYOffset;
2217 break;
2218 }
2219
2221 {
2222 context.painter()->setFont( fragmentFont );
2223 context.painter()->setPen( textColor );
2224 context.painter()->setRenderHint( QPainter::TextAntialiasing );
2225
2226 double partYOffset = 0.0;
2227 for ( const QString &part : parts )
2228 {
2229 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2230 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2231 context.painter()->drawText( QPointF( partXOffset * fontScale, ( currentBlockYOffset + partYOffset ) * fontScale ), part );
2232 context.painter()->scale( fontScale, fontScale );
2233 partYOffset += fragmentMetrics.ascent() / fontScale + letterSpacing;
2234 }
2235 currentBlockYOffset += partYOffset;
2236 }
2237 }
2238 }
2239 fragmentIndex++;
2240 }
2241
2242 if ( maskPainter )
2243 maskPainter->restore();
2244 blockIndex++;
2245 }
2246}
2247
2248double QgsTextRenderer::calculateScaleFactorForFormat( const QgsRenderContext &context, const QgsTextFormat &format )
2249{
2251 return 1.0;
2252
2253 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2254
2255 // THESE THRESHOLD MAY NEED TWEAKING!
2256
2257 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2258 if ( pixelSize < 50 )
2259 return FONT_WORKAROUND_SCALE;
2260 //... but for font sizes we might run into https://bugreports.qt.io/browse/QTBUG-98778, which messes up the spacing between words for large fonts!
2261 // 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
2262 else if ( pixelSize > 200 )
2263 return 200 / pixelSize;
2264 else
2265 return 1.0;
2266}
2267
TextLayoutMode
Text layout modes.
Definition qgis.h:2483
@ 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:2911
TextOrientation
Text orientations.
Definition qgis.h:2468
@ 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.
@ 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:4494
@ 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 ...
TextVerticalAlignment
Text vertical alignment.
Definition qgis.h:2535
@ Bottom
Align to bottom.
@ VerticalCenter
Center align.
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:2516
@ WrapLines
Automatically wrap long lines of text.
TextComponent
Text components.
Definition qgis.h:2500
@ Shadow
Drop shadow.
@ Buffer
Buffer component.
@ Text
Text component.
@ Background
Background shape.
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, exception handling*.
Definition qgsgeos.h:137
Line string geometry type, with support for z-dimension and m-values.
static 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 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.
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.
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.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
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.
static QgsTextDocumentMetrics calculateMetrics(const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor=1.0)
Returns precalculated text metrics for a text document, when rendered using the given base format and...
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.
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.
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.
static QgsTextDocument fromHtml(const QStringList &lines)
Constructor for QgsTextDocument consisting of a set of HTML formatted lines.
static QgsTextDocument fromPlainText(const QStringList &lines)
Constructor for QgsTextDocument consisting of a set of plain text lines.
void append(const QgsTextBlock &block)
Appends a block to the document.
void applyCapitalization(Qgis::Capitalization capitalization)
Applies a capitalization style to the document's text.
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.
bool allowHtmlFormatting() const
Returns true if text should be treated as a HTML document and HTML tags should be used for formatting...
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.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
Stores a fragment of text 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 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 constexpr double FONT_WORKAROUND_SCALE
Scale factor for upscaling font sizes and downscaling destination painter devices.
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
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5465
const char * finder(const char *name)
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30
Q_GUI_EXPORT int qt_defaultDpiX()
Q_GUI_EXPORT int qt_defaultDpiY()