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