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