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