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