QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 ( component.extraWordSpacing || component.extraLetterSpacing )
614 applyExtraSpacingForLineJustification( fragmentFont, component.extraWordSpacing, component.extraLetterSpacing );
615
616 const double yOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
617 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
618
619 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode );
620
621 fragmentIndex++;
622 }
623 advance = xOffset;
624 break;
625 }
626
629 {
630 double partYOffset = component.offset.y() * scaleFactor;
631
632 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
633 double partLastDescent = 0;
634
635 int fragmentIndex = 0;
636 for ( const QgsTextFragment &fragment : component.block )
637 {
638 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
639 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
640
641 const QFontMetricsF fragmentMetrics( fragmentFont );
642
643 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
644
645 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
646 for ( const QString &part : parts )
647 {
648 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
649 partYOffset += fragmentMetrics.ascent() / scaleFactor;
650 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
651 partYOffset += letterSpacing;
652 }
653 partLastDescent = fragmentMetrics.descent() / scaleFactor;
654
655 fragmentIndex++;
656 }
657 height = partYOffset + partLastDescent;
658 advance = partYOffset - component.offset.y() * scaleFactor;
659 break;
660 }
661 }
662
663 QColor bufferColor = buffer.color();
664 bufferColor.setAlphaF( buffer.opacity() );
665 QPen pen( bufferColor );
666 pen.setWidthF( penSize * scaleFactor );
667 pen.setJoinStyle( buffer.joinStyle() );
668 QColor tmpColor( bufferColor );
669 // honor pref for whether to fill buffer interior
670 if ( !buffer.fillBufferInterior() )
671 {
672 tmpColor.setAlpha( 0 );
673 }
674
675 // store buffer's drawing in QPicture for drop shadow call
676 QPicture buffPict;
677 QPainter buffp;
678 buffp.begin( &buffPict );
679 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
680 {
681 context.setPainter( &buffp );
682 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
683
684 tmpEffect->begin( context );
685 context.painter()->setPen( pen );
686 context.painter()->setBrush( tmpColor );
687 if ( scaleFactor != 1.0 )
688 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
689 context.painter()->drawPath( path );
690 if ( scaleFactor != 1.0 )
691 context.painter()->scale( scaleFactor, scaleFactor );
692 tmpEffect->end( context );
693
694 context.setPainter( p );
695 }
696 else
697 {
698 if ( scaleFactor != 1.0 )
699 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
700 buffp.setPen( pen );
701 buffp.setBrush( tmpColor );
702 buffp.drawPath( path );
703 }
704 buffp.end();
705
707 {
708 QgsTextRenderer::Component bufferComponent = component;
709 bufferComponent.origin = QPointF( 0.0, 0.0 );
710 bufferComponent.picture = buffPict;
711 bufferComponent.pictureBuffer = penSize / 2.0;
712 bufferComponent.size.setHeight( height );
713
715 {
716 bufferComponent.offset.setY( - bufferComponent.size.height() );
717 }
718 drawShadow( context, bufferComponent, format );
719 }
720
721 QgsScopedQPainterState painterState( p );
722 context.setPainterFlagsUsingContext( p );
723
724 if ( context.useAdvancedEffects() )
725 {
726 p->setCompositionMode( buffer.blendMode() );
727 }
728
729 // scale for any print output or image saving @ specific dpi
730 p->scale( component.dpiRatio, component.dpiRatio );
731 _fixQPictureDPI( p );
732 p->drawPicture( 0, 0, buffPict );
733
734 return advance / scaleFactor;
735}
736
737void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics,
739{
740 QgsTextMaskSettings mask = format.mask();
741
742 // the mask is drawn to a side painter
743 // or to the main painter for preview
744 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
745 if ( ! p )
746 return;
747
748 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage
749 ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
750 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
751
752 // buffer: draw the text with a big pen
753 QPainterPath path;
754 path.setFillRule( Qt::WindingFill );
755
756 const double scaleFactor = calculateScaleFactorForFormat( context, format );
757
758 // TODO: vertical text mode was ignored when masking feature was added.
759 // Hopefully Oslandia come back and fix this? Hint hint...
760
761 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
762 if ( mode == Qgis::TextLayoutMode::Labeling )
763 {
764 // label size has already been calculated using any symbology reference scale factor -- we need
765 // to temporarily remove the reference scale here or we'll be applying the scaling twice
766 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
767 }
768
769 if ( metrics.isNullFontSize() )
770 return;
771
772 referenceScaleOverride.reset();
773
774 double xOffset = 0;
775 int fragmentIndex = 0;
776 for ( const QgsTextFragment &fragment : component.block )
777 {
778 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
779
780 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
781 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
782
783 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode );
784 fragmentIndex++;
785 }
786
787 QColor bufferColor( Qt::gray );
788 bufferColor.setAlphaF( mask.opacity() );
789
790 QPen pen;
791 QBrush brush;
792 brush.setColor( bufferColor );
793 pen.setColor( bufferColor );
794 pen.setWidthF( penSize * scaleFactor );
795 pen.setJoinStyle( mask.joinStyle() );
796
797 QgsScopedQPainterState painterState( p );
798 context.setPainterFlagsUsingContext( p );
799
800 // scale for any print output or image saving @ specific dpi
801 p->scale( component.dpiRatio, component.dpiRatio );
802 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
803 {
804 QgsPainterSwapper swapper( context, p );
805 {
806 QgsEffectPainter effectPainter( context, mask.paintEffect() );
807 if ( scaleFactor != 1.0 )
808 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
809 context.painter()->setPen( pen );
810 context.painter()->setBrush( brush );
811 context.painter()->drawPath( path );
812 if ( scaleFactor != 1.0 )
813 context.painter()->scale( scaleFactor, scaleFactor );
814 }
815 }
816 else
817 {
818 if ( scaleFactor != 1.0 )
819 p->scale( 1 / scaleFactor, 1 / scaleFactor );
820 p->setPen( pen );
821 p->setBrush( brush );
822 p->drawPath( path );
823 if ( scaleFactor != 1.0 )
824 p->scale( scaleFactor, scaleFactor );
825
826 }
827}
828
829double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
830{
831 QgsTextDocument doc;
832 if ( !format.allowHtmlFormatting() )
833 {
834 doc = QgsTextDocument::fromPlainText( textLines );
835 }
836 else
837 {
838 doc = QgsTextDocument::fromHtml( textLines );
839 }
840 if ( doc.size() == 0 )
841 return 0;
842
843 doc.applyCapitalization( format.capitalization() );
844 return textWidth( context, format, doc );
845}
846
847double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
848{
849 //calculate max width of text lines
850 const double scaleFactor = calculateScaleFactorForFormat( context, format );
851
852 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
853
854 // width doesn't change depending on layout mode, we can use anything here
855 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
856}
857
858double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth )
859{
860 QStringList lines;
861 for ( const QString &line : textLines )
862 {
863 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
864 {
865 lines.append( wrappedText( context, line, maxLineWidth, format ) );
866 }
867 else
868 {
869 lines.append( line );
870 }
871 }
872
873 if ( !format.allowHtmlFormatting() )
874 {
875 return textHeight( context, format, QgsTextDocument::fromPlainText( lines ), mode );
876 }
877 else
878 {
879 return textHeight( context, format, QgsTextDocument::fromHtml( lines ), mode );
880 }
881}
882
883double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
884{
885 const double scaleFactor = calculateScaleFactorForFormat( context, format );
886
887 bool isNullSize = false;
888 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
889 if ( isNullSize )
890 return 0;
891
892 const QFontMetrics fm( baseFont );
893 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
894
895 if ( !includeEffects )
896 return height;
897
898 double maxExtension = 0;
899 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
900 if ( format.buffer().enabled() )
901 {
902 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
903 ? fontSize * format.buffer().size() / 100
904 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
905 }
906 if ( format.shadow().enabled() )
907 {
908 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
909 ? fontSize * format.shadow().offsetDistance() / 100
910 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
911 )
913 ? fontSize * format.shadow().blurRadius() / 100
914 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
915 );
916 }
917 if ( format.background().enabled() )
918 {
919 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
921 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
922 {
923 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
924 }
925 }
926
927 return height + maxExtension;
928}
929
930bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
931{
932 if ( qgsDoubleNear( width, 0.0 ) )
933 return false;
934
935 const QStringList multiLineSplit = text.split( '\n' );
936 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
937 return currentTextWidth > width;
938}
939
940QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
941{
942 const QStringList lines = text.split( '\n' );
943 QStringList outLines;
944 for ( const QString &line : lines )
945 {
946 if ( textRequiresWrapping( context, line, width, format ) )
947 {
948 //first step is to identify words which must be on their own line (too long to fit)
949 const QStringList words = line.split( ' ' );
950 QStringList linesToProcess;
951 QString wordsInCurrentLine;
952 for ( const QString &word : words )
953 {
954 if ( textRequiresWrapping( context, word, width, format ) )
955 {
956 //too long to fit
957 if ( !wordsInCurrentLine.isEmpty() )
958 linesToProcess << wordsInCurrentLine;
959 wordsInCurrentLine.clear();
960 linesToProcess << word;
961 }
962 else
963 {
964 if ( !wordsInCurrentLine.isEmpty() )
965 wordsInCurrentLine.append( ' ' );
966 wordsInCurrentLine.append( word );
967 }
968 }
969 if ( !wordsInCurrentLine.isEmpty() )
970 linesToProcess << wordsInCurrentLine;
971
972 for ( const QString &line : std::as_const( linesToProcess ) )
973 {
974 QString remainingText = line;
975 int lastPos = remainingText.lastIndexOf( ' ' );
976 while ( lastPos > -1 )
977 {
978 //check if remaining text is short enough to go in one line
979 if ( !textRequiresWrapping( context, remainingText, width, format ) )
980 {
981 break;
982 }
983
984 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
985 {
986 outLines << remainingText.left( lastPos );
987 remainingText = remainingText.mid( lastPos + 1 );
988 lastPos = 0;
989 }
990 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
991 }
992 outLines << remainingText;
993 }
994 }
995 else
996 {
997 outLines << line;
998 }
999 }
1000
1001 return outLines;
1002}
1003
1004double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
1005{
1006 QgsTextDocument document = doc;
1007 document.applyCapitalization( format.capitalization() );
1008
1009 //calculate max height of text lines
1010 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1011
1012 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
1013 if ( metrics.isNullFontSize() )
1014 return 0;
1015
1016 return metrics.documentSize( mode, format.orientation() ).height();
1017}
1018
1019void QgsTextRenderer::drawBackground( QgsRenderContext &context, QgsTextRenderer::Component component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
1020{
1021 QgsTextBackgroundSettings background = format.background();
1022
1023 QPainter *prevP = context.painter();
1024 QPainter *p = context.painter();
1025 std::unique_ptr< QgsPaintEffect > tmpEffect;
1026 if ( background.paintEffect() && background.paintEffect()->enabled() )
1027 {
1028 tmpEffect.reset( background.paintEffect()->clone() );
1029 tmpEffect->begin( context );
1030 p = context.painter();
1031 }
1032
1033 //QgsDebugMsgLevel( QStringLiteral( "Background label rotation: %1" ).arg( component.rotation() ), 4 );
1034
1035 // shared calculations between shapes and SVG
1036
1037 // configure angles, set component rotation and rotationOffset
1038 const double originAdjustRotationRadians = -component.rotation;
1040 {
1041 component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
1042 component.rotationOffset =
1043 background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
1044 }
1045 else // RotationFixed
1046 {
1047 component.rotation = 0.0; // don't use label's rotation
1048 component.rotationOffset = background.rotation();
1049 }
1050
1051 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1052
1053 if ( mode != Qgis::TextLayoutMode::Labeling )
1054 {
1055 // need to calculate size of text
1056 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1057 double width = documentSize.width();
1058 double height = documentSize.height();
1059
1060 switch ( mode )
1061 {
1065 switch ( component.hAlign )
1066 {
1069 component.center = QPointF( component.origin.x() + width / 2.0,
1070 component.origin.y() + height / 2.0 );
1071 break;
1072
1074 component.center = QPointF( component.origin.x() + component.size.width() / 2.0,
1075 component.origin.y() + height / 2.0 );
1076 break;
1077
1079 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0,
1080 component.origin.y() + height / 2.0 );
1081 break;
1082 }
1083 break;
1084
1086 {
1087 bool isNullSize = false;
1088 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1089 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1090 switch ( component.hAlign )
1091 {
1094 component.center = QPointF( component.origin.x() + width / 2.0,
1095 component.origin.y() - height / 2.0 + originAdjust );
1096 break;
1097
1099 component.center = QPointF( component.origin.x(),
1100 component.origin.y() - height / 2.0 + originAdjust );
1101 break;
1102
1104 component.center = QPointF( component.origin.x() - width / 2.0,
1105 component.origin.y() - height / 2.0 + originAdjust );
1106 break;
1107 }
1108
1109 // apply rotation to center point
1110 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1111 {
1112 const double dx = component.center.x() - component.origin.x();
1113 const double dy = component.center.y() - component.origin.y();
1114 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1115 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1116 }
1117 break;
1118 }
1119
1121 break;
1122 }
1123
1125 component.size = QSizeF( width, height );
1126 }
1127
1128 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1129
1130 switch ( background.type() )
1131 {
1134 {
1135 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1136
1137 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1138 return;
1139
1140 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1141 return;
1142
1143 double sizeOut = 0.0;
1144 {
1145 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1146
1147 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1148 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1149 {
1150 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1151 }
1152 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1153 {
1154 sizeOut = std::max( component.size.width(), component.size.height() );
1155 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1156
1157 // add buffer
1158 sizeOut += bufferSize * 2;
1159 }
1160 }
1161
1162 // don't bother rendering symbols smaller than 1x1 pixels in size
1163 // TODO: add option to not show any svgs under/over a certain size
1164 if ( sizeOut < 1.0 )
1165 return;
1166
1167 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1168 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1169 {
1170 QVariantMap map; // for SVG symbology marker
1171 map[QStringLiteral( "name" )] = background.svgFile().trimmed();
1172 map[QStringLiteral( "size" )] = QString::number( sizeOut );
1173 map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1174 map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter
1175
1176 // offset is handled by this local painter
1177 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1178 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1179 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1180 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1181
1182 map[QStringLiteral( "fill" )] = background.fillColor().name();
1183 map[QStringLiteral( "outline" )] = background.strokeColor().name();
1184 map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() );
1185 map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1186
1188 {
1189 QgsTextShadowSettings shadow = format.shadow();
1190 // configure SVG shadow specs
1191 QVariantMap shdwmap( map );
1192 shdwmap[QStringLiteral( "fill" )] = shadow.color().name();
1193 shdwmap[QStringLiteral( "outline" )] = shadow.color().name();
1194 shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut );
1195
1196 // store SVG's drawing in QPicture for drop shadow call
1197 QPicture svgPict;
1198 QPainter svgp;
1199 svgp.begin( &svgPict );
1200
1201 // draw shadow symbol
1202
1203 // clone current render context map unit/mm conversion factors, but not
1204 // other map canvas parameters, then substitute this painter for use in symbology painting
1205 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1206 // but will be created relative to the SVG's computed size, not the current map canvas
1207 QgsRenderContext shdwContext;
1208 shdwContext.setMapToPixel( context.mapToPixel() );
1209 shdwContext.setScaleFactor( context.scaleFactor() );
1210 shdwContext.setPainter( &svgp );
1211
1212 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1213 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1214 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1215
1216 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1217 svgp.end();
1218
1219 component.picture = svgPict;
1220 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1221 component.pictureBuffer = 0.0;
1222
1223 component.size = QSizeF( sizeOut, sizeOut );
1224 component.offset = QPointF( 0.0, 0.0 );
1225
1226 // rotate about origin center of SVG
1227 QgsScopedQPainterState painterState( p );
1228 context.setPainterFlagsUsingContext( p );
1229
1230 p->translate( component.center.x(), component.center.y() );
1231 p->rotate( component.rotation );
1232 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1233 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1234 p->translate( QPointF( xoff, yoff ) );
1235 p->rotate( component.rotationOffset );
1236 p->translate( -sizeOut / 2, sizeOut / 2 );
1237
1238 drawShadow( context, component, format );
1239 }
1240 renderedSymbol.reset( );
1241
1243 renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) );
1244 }
1245 else
1246 {
1247 renderedSymbol.reset( background.markerSymbol()->clone() );
1248 renderedSymbol->setSize( sizeOut );
1249 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1250 }
1251
1252 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1253
1254 // draw the actual symbol
1255 QgsScopedQPainterState painterState( p );
1256 context.setPainterFlagsUsingContext( p );
1257
1258 if ( context.useAdvancedEffects() )
1259 {
1260 p->setCompositionMode( background.blendMode() );
1261 }
1262 p->translate( component.center.x(), component.center.y() );
1263 p->rotate( component.rotation );
1264 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1265 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1266 p->translate( QPointF( xoff, yoff ) );
1267 p->rotate( component.rotationOffset );
1268
1269 const QgsFeature f = context.expressionContext().feature();
1270 renderedSymbol->startRender( context, context.expressionContext().fields() );
1271 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1272 renderedSymbol->stopRender( context );
1273 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1274
1275 break;
1276 }
1277
1282 {
1283 double w = component.size.width();
1284 double h = component.size.height();
1285
1286 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1287 {
1288 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1289 background.sizeMapUnitScale() );
1290 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1291 background.sizeMapUnitScale() );
1292 }
1293 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1294 {
1295 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1296 {
1297 if ( w > h )
1298 h = w;
1299 else if ( h > w )
1300 w = h;
1301 }
1302 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1303 {
1304 // start with label bound by circle
1305 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1306 w = h;
1307 }
1308 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1309 {
1310 // start with label bound by ellipse
1311 h = h * M_SQRT1_2 * 2;
1312 w = w * M_SQRT1_2 * 2;
1313 }
1314
1315 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1316 background.sizeMapUnitScale() );
1317 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1318 background.sizeMapUnitScale() );
1319
1320 w += bufferWidth * 2;
1321 h += bufferHeight * 2;
1322 }
1323
1324 // offsets match those of symbology: -x = left, -y = up
1325 QRectF rect( -w / 2.0, - h / 2.0, w, h );
1326
1327 if ( rect.isNull() )
1328 return;
1329
1330 QgsScopedQPainterState painterState( p );
1331 context.setPainterFlagsUsingContext( p );
1332
1333 p->translate( QPointF( component.center.x(), component.center.y() ) );
1334 p->rotate( component.rotation );
1335 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1336 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1337 p->translate( QPointF( xoff, yoff ) );
1338 p->rotate( component.rotationOffset );
1339
1340 QPainterPath path;
1341
1342 // Paths with curves must be enlarged before conversion to QPolygonF, or
1343 // the curves are approximated too much and appear jaggy
1344 QTransform t = QTransform::fromScale( 10, 10 );
1345 // inverse transform used to scale created polygons back to expected size
1346 QTransform ti = t.inverted();
1347
1349 || background.type() == QgsTextBackgroundSettings::ShapeSquare )
1350 {
1351 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1352 {
1353 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1354 }
1355 else
1356 {
1357 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1358 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1359 path.addRoundedRect( rect, xRadius, yRadius );
1360 }
1361 }
1362 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse
1363 || background.type() == QgsTextBackgroundSettings::ShapeCircle )
1364 {
1365 path.addEllipse( rect );
1366 }
1367 QPolygonF tempPolygon = path.toFillPolygon( t );
1368 QPolygonF polygon = ti.map( tempPolygon );
1369 QPicture shapePict;
1370 QPainter *oldp = context.painter();
1371 QPainter shapep;
1372
1373 shapep.begin( &shapePict );
1374 context.setPainter( &shapep );
1375
1376 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1377 renderedSymbol.reset( background.fillSymbol()->clone() );
1378 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1379
1380 const QgsFeature f = context.expressionContext().feature();
1381 renderedSymbol->startRender( context, context.expressionContext().fields() );
1382 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1383 renderedSymbol->stopRender( context );
1384
1385 shapep.end();
1386 context.setPainter( oldp );
1387
1389 {
1390 component.picture = shapePict;
1391 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1392
1393 component.size = rect.size();
1394 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1395 drawShadow( context, component, format );
1396 }
1397
1398 if ( context.useAdvancedEffects() )
1399 {
1400 p->setCompositionMode( background.blendMode() );
1401 }
1402
1403 // scale for any print output or image saving @ specific dpi
1404 p->scale( component.dpiRatio, component.dpiRatio );
1405 _fixQPictureDPI( p );
1406 p->drawPicture( 0, 0, shapePict );
1407 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1408 break;
1409 }
1410 }
1411
1412 if ( tmpEffect )
1413 {
1414 tmpEffect->end( context );
1415 context.setPainter( prevP );
1416 }
1417}
1418
1419void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1420{
1421 QgsTextShadowSettings shadow = format.shadow();
1422
1423 // incoming component sizes should be multiplied by rasterCompressFactor, as
1424 // this allows shadows to be created at paint device dpi (e.g. high resolution),
1425 // then scale device painter by 1.0 / rasterCompressFactor for output
1426
1427 QPainter *p = context.painter();
1428 const double componentWidth = component.size.width();
1429 const double componentHeight = component.size.height();
1430 double xOffset = component.offset.x(), yOffset = component.offset.y();
1431 double pictbuffer = component.pictureBuffer;
1432
1433 // generate pixmap representation of label component drawing
1434 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1435
1436 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1437 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage
1438 ? fontSize * shadow.blurRadius() / 100
1439 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1440 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1441 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1442
1443 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1444 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1445 double blurBufferClippingScale = 3.75;
1446 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1447
1448 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1449 componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1450 QImage::Format_ARGB32_Premultiplied );
1451
1452 // TODO: add labeling gui option to not show any shadows under/over a certain size
1453 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1454 int minBlurImgSize = 1;
1455 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1456 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1457 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1458 int maxBlurImgSize = 40000;
1459 if ( blurImg.isNull()
1460 || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize )
1461 || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1462 return;
1463
1464 blurImg.fill( QColor( Qt::transparent ).rgba() );
1465 QPainter pictp;
1466 if ( !pictp.begin( &blurImg ) )
1467 return;
1468 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1469 QPointF imgOffset( blurbuffer + pictbuffer + xOffset,
1470 blurbuffer + pictbuffer + componentHeight + yOffset );
1471
1472 pictp.drawPicture( imgOffset,
1473 component.picture );
1474
1475 // overlay shadow color
1476 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1477 pictp.fillRect( blurImg.rect(), shadow.color() );
1478 pictp.end();
1479
1480 // blur the QImage in-place
1481 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1482 {
1483 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1484 }
1485
1486#if 0
1487 // debug rect for QImage shadow registration and clipping visualization
1488 QPainter picti;
1489 picti.begin( &blurImg );
1490 picti.setBrush( Qt::Dense7Pattern );
1491 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1492 imgPen.setWidth( 1 );
1493 picti.setPen( imgPen );
1494 picti.setOpacity( 0.1 );
1495 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1496 picti.end();
1497#endif
1498
1499 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage
1500 ? fontSize * shadow.offsetDistance() / 100
1501 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1502 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1503 if ( shadow.offsetGlobal() )
1504 {
1505 // TODO: check for differences in rotation origin and cw/ccw direction,
1506 // when this shadow function is used for something other than labels
1507
1508 // it's 0-->cw-->360 for labels
1509 //QgsDebugMsgLevel( QStringLiteral( "Shadow aggregated label rotation (degrees): %1" ).arg( component.rotation() + component.rotationOffset() ), 4 );
1510 angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1511 }
1512
1513 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ),
1514 -offsetDist * std::sin( angleRad + M_PI_2 ) );
1515
1516 p->save();
1517 context.setPainterFlagsUsingContext( p );
1518 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1519 // corresponding flag in the render context instead...
1520 p->setRenderHint( QPainter::SmoothPixmapTransform );
1521 if ( context.useAdvancedEffects() )
1522 {
1523 p->setCompositionMode( shadow.blendMode() );
1524 }
1525 p->setOpacity( shadow.opacity() );
1526
1527 double scale = shadow.scale() / 100.0;
1528 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1529 p->scale( scale, scale );
1530 if ( component.useOrigin )
1531 {
1532 p->translate( component.origin.x(), component.origin.y() );
1533 }
1534 p->translate( transPt );
1535 p->translate( -imgOffset.x(),
1536 -imgOffset.y() );
1537 p->drawImage( 0, 0, blurImg );
1538 p->restore();
1539
1540 // debug rects
1541#if 0
1542 // draw debug rect for QImage painting registration
1543 p->save();
1544 p->setBrush( Qt::NoBrush );
1545 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1546 imgPen.setWidth( 2 );
1547 imgPen.setStyle( Qt::DashLine );
1548 p->setPen( imgPen );
1549 p->scale( scale, scale );
1550 if ( component.useOrigin() )
1551 {
1552 p->translate( component.origin().x(), component.origin().y() );
1553 }
1554 p->translate( transPt );
1555 p->translate( -imgOffset.x(),
1556 -imgOffset.y() );
1557 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1558 p->restore();
1559
1560 // draw debug rect for passed in component dimensions
1561 p->save();
1562 p->setBrush( Qt::NoBrush );
1563 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1564 componentRectPen.setWidth( 1 );
1565 if ( component.useOrigin() )
1566 {
1567 p->translate( component.origin().x(), component.origin().y() );
1568 }
1569 p->setPen( componentRectPen );
1570 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1571 p->restore();
1572#endif
1573}
1574
1575
1576void QgsTextRenderer::drawTextInternal( Qgis::TextComponent drawType,
1577 QgsRenderContext &context,
1578 const QgsTextFormat &format,
1579 const Component &component,
1580 const QgsTextDocument &document,
1581 const QgsTextDocumentMetrics &metrics,
1583{
1584 if ( !context.painter() )
1585 {
1586 return;
1587 }
1588
1589 const double fontScale = calculateScaleFactorForFormat( context, format );
1590
1591 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1592 if ( mode == Qgis::TextLayoutMode::Labeling )
1593 {
1594 // label size has already been calculated using any symbology reference scale factor -- we need
1595 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1596 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1597 }
1598
1599 if ( metrics.isNullFontSize() )
1600 return;
1601
1602 referenceScaleOverride.reset();
1603
1604 double rotation = 0;
1605 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1606 switch ( orientation )
1607 {
1609 {
1610 drawTextInternalHorizontal( context, format, drawType, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1611 break;
1612 }
1613
1616 {
1617 drawTextInternalVertical( context, format, drawType, mode, component, document, metrics, fontScale, alignment, vAlignment, rotation );
1618 break;
1619 }
1620 }
1621}
1622
1623Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1624{
1625 rotation = -component.rotation * 180 / M_PI;
1626
1627 switch ( format.orientation() )
1628 {
1630 {
1631 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1632 if ( rotation >= -315 && rotation < -90 )
1633 {
1634 rotation -= 90;
1636 }
1637 else if ( rotation >= -90 && rotation < -45 )
1638 {
1639 rotation += 90;
1641 }
1642
1644 }
1645
1648 return format.orientation();
1649 }
1651}
1652
1653void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1654{
1655 const QString blockText = block.toPlainText();
1656 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1657 finder.toStart();
1658 int wordBoundaries = 0;
1659 while ( finder.toNextBoundary() != -1 )
1660 {
1661 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1662 wordBoundaries++;
1663 }
1664
1665 if ( wordBoundaries > 0 )
1666 {
1667 // word boundaries found => justify by padding word spacing
1668 extraWordSpace = spaceToDistribute / wordBoundaries;
1669 }
1670 else
1671 {
1672 // no word boundaries found => justify by letter spacing
1673 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1674 finder.toStart();
1675
1676 int graphemeBoundaries = 0;
1677 while ( finder.toNextBoundary() != -1 )
1678 {
1679 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1680 graphemeBoundaries++;
1681 }
1682
1683 if ( graphemeBoundaries > 0 )
1684 {
1685 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1686 }
1687 }
1688}
1689
1690void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1691{
1692 const double prevWordSpace = font.wordSpacing();
1693 font.setWordSpacing( prevWordSpace + extraWordSpace );
1694 const double prevLetterSpace = font.letterSpacing();
1695 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1696}
1697
1698void 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,
1699 Qgis::TextVerticalAlignment vAlignment, double rotation )
1700{
1701 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1702 const QStringList textLines = document.toPlainText();
1703
1704 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
1705
1706 double labelWidest = 0.0;
1707 switch ( mode )
1708 {
1711 labelWidest = documentSize.width();
1712 break;
1713
1717 labelWidest = component.size.width();
1718 break;
1719 }
1720
1721 double verticalAlignOffset = 0;
1722
1723 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
1724
1726 {
1727 const double overallHeight = documentSize.height();
1728 switch ( vAlignment )
1729 {
1731 break;
1732
1734 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5;
1735 break;
1736
1738 verticalAlignOffset = ( component.size.height() - overallHeight );
1739 break;
1740 }
1741 }
1742
1743 int blockIndex = 0;
1744 for ( const QgsTextBlock &block : document )
1745 {
1746 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 )
1747 || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1748
1749 const double blockHeight = metrics.blockHeight( blockIndex );
1750
1751 QgsScopedQPainterState painterState( context.painter() );
1753 context.painter()->translate( component.origin );
1754 if ( !qgsDoubleNear( rotation, 0.0 ) )
1755 context.painter()->rotate( rotation );
1756
1757 // apply to the mask painter the same transformations
1758 if ( maskPainter )
1759 {
1760 maskPainter->save();
1761 maskPainter->translate( component.origin );
1762 if ( !qgsDoubleNear( rotation, 0.0 ) )
1763 maskPainter->rotate( rotation );
1764 }
1765
1766 // figure x offset for horizontal alignment of multiple lines
1767 double xMultiLineOffset = 0.0;
1768 double blockWidth = metrics.blockWidth( blockIndex );
1769 double extraWordSpace = 0;
1770 double extraLetterSpace = 0;
1771 if ( adjustForAlignment )
1772 {
1773 double labelWidthDiff = 0;
1774 switch ( hAlignment )
1775 {
1777 labelWidthDiff = ( labelWidest - blockWidth ) * 0.5;
1778 break;
1779
1781 labelWidthDiff = labelWidest - blockWidth;
1782 break;
1783
1785 if ( !isFinalLineInParagraph && labelWidest > blockWidth )
1786 {
1787 calculateExtraSpacingForLineJustification( labelWidest - blockWidth, block, extraWordSpace, extraLetterSpace );
1788 blockWidth = labelWidest;
1789 }
1790 break;
1791
1793 break;
1794 }
1795
1796 switch ( mode )
1797 {
1802 xMultiLineOffset = labelWidthDiff;
1803 break;
1804
1806 {
1807 switch ( hAlignment )
1808 {
1810 xMultiLineOffset = labelWidthDiff - labelWidest;
1811 break;
1812
1814 xMultiLineOffset = labelWidthDiff - labelWidest / 2.0;
1815 break;
1816
1819 break;
1820 }
1821 }
1822 break;
1823 }
1824 }
1825
1826 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1827
1828 context.painter()->translate( QPointF( xMultiLineOffset, baseLineOffset + verticalAlignOffset ) );
1829 if ( maskPainter )
1830 maskPainter->translate( QPointF( xMultiLineOffset, baseLineOffset + verticalAlignOffset ) );
1831
1832 Component subComponent;
1833 subComponent.block = block;
1834 subComponent.blockIndex = blockIndex;
1835 subComponent.size = QSizeF( blockWidth, blockHeight );
1836 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
1837 subComponent.rotation = -component.rotation * 180 / M_PI;
1838 subComponent.rotationOffset = 0.0;
1839 subComponent.extraWordSpacing = extraWordSpace * fontScale;
1840 subComponent.extraLetterSpacing = extraLetterSpace * fontScale;
1841
1842 // draw the mask below the text (for preview)
1843 if ( format.mask().enabled() )
1844 {
1845 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
1846 }
1847
1848 if ( drawType == Qgis::TextComponent::Buffer )
1849 {
1850 QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
1851 }
1852 else
1853 {
1854 // store text's drawing in QPicture for drop shadow call
1855 QPicture textPict;
1856 QPainter textp;
1857 textp.begin( &textPict );
1858 textp.setPen( Qt::NoPen );
1859
1860 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1861 if ( mode == Qgis::TextLayoutMode::Labeling )
1862 {
1863 // label size has already been calculated using any symbology reference scale factor -- we need
1864 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1865 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1866 }
1867
1868 referenceScaleOverride.reset();
1869
1870 if ( !metrics.isNullFontSize() )
1871 {
1872 textp.scale( 1 / fontScale, 1 / fontScale );
1873
1874 double xOffset = 0;
1875 int fragmentIndex = 0;
1876 for ( const QgsTextFragment &fragment : block )
1877 {
1878 // draw text, QPainterPath method
1879 QPainterPath path;
1880 path.setFillRule( Qt::WindingFill );
1881
1882 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1883
1884 if ( extraWordSpace || extraLetterSpace )
1885 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1886
1887 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1888
1889 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1890
1891 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1892 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1893 textp.setBrush( textColor );
1894 textp.drawPath( path );
1895
1896 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1897 fragmentIndex ++;
1898 }
1899 textp.end();
1900 }
1901
1902 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
1903 {
1904 subComponent.picture = textPict;
1905 subComponent.pictureBuffer = 0.0; // no pen width to deal with
1906 subComponent.origin = QPointF( 0.0, 0.0 );
1907
1908 QgsTextRenderer::drawShadow( context, subComponent, format );
1909 }
1910
1911 // paint the text
1912 if ( context.useAdvancedEffects() )
1913 {
1914 context.painter()->setCompositionMode( format.blendMode() );
1915 }
1916
1917 // scale for any print output or image saving @ specific dpi
1918 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
1919
1920 switch ( context.textRenderFormat() )
1921 {
1923 {
1924 // draw outlined text
1925 _fixQPictureDPI( context.painter() );
1926 context.painter()->drawPicture( 0, 0, textPict );
1927 break;
1928 }
1929
1931 {
1932 double xOffset = 0;
1933 int fragmentIndex = 0;
1934 for ( const QgsTextFragment &fragment : block )
1935 {
1936 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1937
1938 if ( extraWordSpace || extraLetterSpace )
1939 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1940
1941 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1942
1943 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1944 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1945
1946 context.painter()->setPen( textColor );
1947 context.painter()->setFont( fragmentFont );
1948 context.painter()->setRenderHint( QPainter::TextAntialiasing );
1949
1950 context.painter()->scale( 1 / fontScale, 1 / fontScale );
1951 context.painter()->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1952 context.painter()->scale( fontScale, fontScale );
1953
1954 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
1955 fragmentIndex++;
1956 }
1957 }
1958 }
1959 }
1960 if ( maskPainter )
1961 maskPainter->restore();
1962
1963 blockIndex++;
1964 }
1965}
1966
1967void 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 )
1968{
1969 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1970 const QStringList textLines = document.toPlainText();
1971
1972 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1973 if ( mode == Qgis::TextLayoutMode::Labeling )
1974 {
1975 // label size has already been calculated using any symbology reference scale factor -- we need
1976 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1977 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1978 }
1979
1980 if ( metrics.isNullFontSize() )
1981 return;
1982
1983 referenceScaleOverride.reset();
1984
1985 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
1986 const double actualTextWidth = documentSize.width();
1987 double textRectWidth = 0.0;
1988
1989 switch ( mode )
1990 {
1993 textRectWidth = actualTextWidth;
1994 break;
1995
1999 textRectWidth = component.size.width();
2000 break;
2001 }
2002
2003 int maxLineLength = 0;
2004 for ( const QString &line : std::as_const( textLines ) )
2005 {
2006 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2007 }
2008
2009 const double actualLabelHeight = documentSize.height();
2010 int blockIndex = 0;
2011
2012 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2013
2014 for ( const QgsTextBlock &block : document )
2015 {
2016 QgsScopedQPainterState painterState( context.painter() );
2018
2019 context.painter()->translate( component.origin );
2020 if ( !qgsDoubleNear( rotation, 0.0 ) )
2021 context.painter()->rotate( rotation );
2022
2023 // apply to the mask painter the same transformations
2024 if ( maskPainter )
2025 {
2026 maskPainter->save();
2027 maskPainter->translate( component.origin );
2028 if ( !qgsDoubleNear( rotation, 0.0 ) )
2029 maskPainter->rotate( rotation );
2030 }
2031
2032 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2033
2034 // figure x offset of multiple lines
2035 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2036 if ( adjustForAlignment )
2037 {
2038 double hAlignmentOffset = 0;
2039 switch ( hAlignment )
2040 {
2042 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2043 break;
2044
2046 hAlignmentOffset = textRectWidth - actualTextWidth;
2047 break;
2048
2051 break;
2052 }
2053
2054 switch ( mode )
2055 {
2060 xOffset += hAlignmentOffset;
2061 break;
2062
2064 break;
2065 }
2066 }
2067
2068 double yOffset = 0.0;
2069 switch ( mode )
2070 {
2073 {
2074 if ( rotation >= -405 && rotation < -180 )
2075 {
2076 yOffset = 0;
2077 }
2078 else if ( rotation >= 0 && rotation < 45 )
2079 {
2080 xOffset -= actualTextWidth;
2081 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2082 }
2083 }
2084 else
2085 {
2086 yOffset = -actualLabelHeight;
2087 }
2088 break;
2089
2091 yOffset = -actualLabelHeight;
2092 break;
2093
2097 yOffset = 0;
2098 break;
2099 }
2100
2101 context.painter()->translate( QPointF( xOffset, yOffset ) );
2102
2103 double currentBlockYOffset = 0;
2104 int fragmentIndex = 0;
2105 for ( const QgsTextFragment &fragment : block )
2106 {
2107 QgsScopedQPainterState fragmentPainterState( context.painter() );
2108
2109 // apply some character replacement to draw symbols in vertical presentation
2110 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2111
2112 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2113
2114 QFontMetricsF fragmentMetrics( fragmentFont );
2115
2116 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2117 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2118
2119 Component subComponent;
2120 subComponent.block = QgsTextBlock( fragment );
2121 subComponent.blockIndex = blockIndex;
2122 subComponent.firstFragmentIndex = fragmentIndex;
2123 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2124 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2125 subComponent.rotation = -component.rotation * 180 / M_PI;
2126 subComponent.rotationOffset = 0.0;
2127
2128 // draw the mask below the text (for preview)
2129 if ( format.mask().enabled() )
2130 {
2131 // WARNING: totally broken! (has been since mask was introduced)
2132#if 0
2133 QgsTextRenderer::drawMask( context, subComponent, format );
2134#endif
2135 }
2136
2137 if ( drawType == Qgis::TextComponent::Buffer )
2138 {
2139 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2140 }
2141 else
2142 {
2143 // draw text, QPainterPath method
2144 QPainterPath path;
2145 path.setFillRule( Qt::WindingFill );
2146 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2147 double partYOffset = 0.0;
2148 for ( const QString &part : parts )
2149 {
2150 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2151 partYOffset += fragmentMetrics.ascent() / fontScale;
2152 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2153 partYOffset += letterSpacing;
2154 }
2155
2156 // store text's drawing in QPicture for drop shadow call
2157 QPicture textPict;
2158 QPainter textp;
2159 textp.begin( &textPict );
2160 textp.setPen( Qt::NoPen );
2161 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2162 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2163 textp.setBrush( textColor );
2164 textp.scale( 1 / fontScale, 1 / fontScale );
2165 textp.drawPath( path );
2166
2167 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2168 // e.g. some capitalization options, but not others
2169 //textp.setFont( tmpLyr.textFont );
2170 //textp.setPen( tmpLyr.textColor );
2171 //textp.drawText( 0, 0, component.text() );
2172 textp.end();
2173
2174 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2175 {
2176 subComponent.picture = textPict;
2177 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2178 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2179 const double prevY = subComponent.offset.y();
2180 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2181 subComponent.useOrigin = true;
2182 QgsTextRenderer::drawShadow( context, subComponent, format );
2183 subComponent.useOrigin = false;
2184 subComponent.offset = QPointF( 0, prevY );
2185 }
2186
2187 // paint the text
2188 if ( context.useAdvancedEffects() )
2189 {
2190 context.painter()->setCompositionMode( format.blendMode() );
2191 }
2192
2193 // scale for any print output or image saving @ specific dpi
2194 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2195
2196 switch ( context.textRenderFormat() )
2197 {
2199 {
2200 // draw outlined text
2201 context.painter()->translate( 0, currentBlockYOffset );
2202 _fixQPictureDPI( context.painter() );
2203 context.painter()->drawPicture( 0, 0, textPict );
2204 currentBlockYOffset += partYOffset;
2205 break;
2206 }
2207
2209 {
2210 context.painter()->setFont( fragmentFont );
2211 context.painter()->setPen( textColor );
2212 context.painter()->setRenderHint( QPainter::TextAntialiasing );
2213
2214 double partYOffset = 0.0;
2215 for ( const QString &part : parts )
2216 {
2217 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2218 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2219 context.painter()->drawText( QPointF( partXOffset * fontScale, ( currentBlockYOffset + partYOffset ) * fontScale ), part );
2220 context.painter()->scale( fontScale, fontScale );
2221 partYOffset += fragmentMetrics.ascent() / fontScale + letterSpacing;
2222 }
2223 currentBlockYOffset += partYOffset;
2224 }
2225 }
2226 }
2227 fragmentIndex++;
2228 }
2229
2230 if ( maskPainter )
2231 maskPainter->restore();
2232 blockIndex++;
2233 }
2234}
2235
2236double QgsTextRenderer::calculateScaleFactorForFormat( const QgsRenderContext &context, const QgsTextFormat &format )
2237{
2239 return 1.0;
2240
2241 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2242
2243 // THESE THRESHOLD MAY NEED TWEAKING!
2244
2245 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2246 if ( pixelSize < 50 )
2247 return FONT_WORKAROUND_SCALE;
2248 //... 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!
2249 // 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
2250 else if ( pixelSize > 200 )
2251 return 200 / pixelSize;
2252 else
2253 return 1.0;
2254}
2255
TextLayoutMode
Text layout modes.
Definition: qgis.h:2383
@ 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:2769
TextOrientation
Text orientations.
Definition: qgis.h:2368
@ 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:4255
@ 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:2435
@ Bottom
Align to bottom.
@ VerticalCenter
Center align.
TextHorizontalAlignment
Text horizontal alignment.
Definition: qgis.h:2416
@ WrapLines
Automatically wrap long lines of text.
TextComponent
Text components.
Definition: qgis.h:2400
@ 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:56
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:98
Line string geometry type, with support for z-dimension and m-values.
Definition: qgslinestring.h:45
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.
Definition: qgstextblock.h:36
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 ...
QColor textColor() const
Returns the character's text color, or an invalid color if no color override is set and the default f...
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.
Definition: qgstextformat.h:41
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.
QString text() const
Returns the text content of the fragment.
const QgsTextCharacterFormat & characterFormat() const
Returns the character formatting for 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)
Definition: MathUtils.cpp:716
Contains geos related utilities and functions.
Definition: qgsgeos.h:36
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:5207
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()