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