QGIS API Documentation  3.21.0-Master (5b68dc587e)
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 = ( context.flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ) ? FONT_WORKAROUND_SCALE : 1.0;
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 = ( context.flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ) ? FONT_WORKAROUND_SCALE : 1.0;
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 = ( context.flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ) ? FONT_WORKAROUND_SCALE : 1.0;
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 = ( context.flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ) ? FONT_WORKAROUND_SCALE : 1.0;
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 = ( context.flags() & Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering ) ? FONT_WORKAROUND_SCALE : 1.0;
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
772  {
773  component.rotation = -( component.rotation * 180 / M_PI ); // RotationSync
774  component.rotationOffset =
775  background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? background.rotation() : 0.0;
776  }
777  else // RotationFixed
778  {
779  component.rotation = 0.0; // don't use label's rotation
780  component.rotationOffset = background.rotation();
781  }
782 
784 
785  if ( mode != Label )
786  {
787  // need to calculate size of text
788  double width = textWidth( context, format, document );
789  double height = textHeight( context, format, document, mode );
790 
791  switch ( mode )
792  {
793  case Rect:
794  switch ( component.hAlign )
795  {
796  case AlignLeft:
797  case AlignJustify:
798  component.center = QPointF( component.origin.x() + width / 2.0,
799  component.origin.y() + height / 2.0 );
800  break;
801 
802  case AlignCenter:
803  component.center = QPointF( component.origin.x() + component.size.width() / 2.0,
804  component.origin.y() + height / 2.0 );
805  break;
806 
807  case AlignRight:
808  component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0,
809  component.origin.y() + height / 2.0 );
810  break;
811  }
812  break;
813 
814  case Point:
815  {
816  bool isNullSize = false;
817  QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
818  double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
819  switch ( component.hAlign )
820  {
821  case AlignLeft:
822  case AlignJustify:
823  component.center = QPointF( component.origin.x() + width / 2.0,
824  component.origin.y() - height / 2.0 + originAdjust );
825  break;
826 
827  case AlignCenter:
828  component.center = QPointF( component.origin.x(),
829  component.origin.y() - height / 2.0 + originAdjust );
830  break;
831 
832  case AlignRight:
833  component.center = QPointF( component.origin.x() - width / 2.0,
834  component.origin.y() - height / 2.0 + originAdjust );
835  break;
836  }
837  break;
838  }
839 
840  case Label:
841  break;
842  }
843 
845  component.size = QSizeF( width, height );
846  }
847 
848  // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
849 
850  switch ( background.type() )
851  {
854  {
855  // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
856 
857  if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
858  return;
859 
860  if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
861  return;
862 
863  double sizeOut = 0.0;
864  // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
865  if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
866  {
867  sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
868  }
869  else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
870  {
871  sizeOut = std::max( component.size.width(), component.size.height() );
872  double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
873 
874  // add buffer
875  sizeOut += bufferSize * 2;
876  }
877 
878  // don't bother rendering symbols smaller than 1x1 pixels in size
879  // TODO: add option to not show any svgs under/over a certain size
880  if ( sizeOut < 1.0 )
881  return;
882 
883  std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
884  if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
885  {
886  QVariantMap map; // for SVG symbology marker
887  map[QStringLiteral( "name" )] = background.svgFile().trimmed();
888  map[QStringLiteral( "size" )] = QString::number( sizeOut );
889  map[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( QgsUnitTypes::RenderPixels );
890  map[QStringLiteral( "angle" )] = QString::number( 0.0 ); // angle is handled by this local painter
891 
892  // offset is handled by this local painter
893  // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
894  //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
895  //map["offset_unit"] = QgsUnitTypes::encodeUnit(
896  // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
897 
898  map[QStringLiteral( "fill" )] = background.fillColor().name();
899  map[QStringLiteral( "outline" )] = background.strokeColor().name();
900  map[QStringLiteral( "outline-width" )] = QString::number( background.strokeWidth() );
901  map[QStringLiteral( "outline_width_unit" )] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
902 
903  if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape )
904  {
905  QgsTextShadowSettings shadow = format.shadow();
906  // configure SVG shadow specs
907  QVariantMap shdwmap( map );
908  shdwmap[QStringLiteral( "fill" )] = shadow.color().name();
909  shdwmap[QStringLiteral( "outline" )] = shadow.color().name();
910  shdwmap[QStringLiteral( "size" )] = QString::number( sizeOut );
911 
912  // store SVG's drawing in QPicture for drop shadow call
913  QPicture svgPict;
914  QPainter svgp;
915  svgp.begin( &svgPict );
916 
917  // draw shadow symbol
918 
919  // clone current render context map unit/mm conversion factors, but not
920  // other map canvas parameters, then substitute this painter for use in symbology painting
921  // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
922  // but will be created relative to the SVG's computed size, not the current map canvas
923  QgsRenderContext shdwContext;
924  shdwContext.setMapToPixel( context.mapToPixel() );
925  shdwContext.setScaleFactor( context.scaleFactor() );
926  shdwContext.setPainter( &svgp );
927 
928  std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
929  QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
930  QgsSymbolRenderContext svgShdwContext( shdwContext, QgsUnitTypes::RenderUnknownUnit, background.opacity() );
931 
932  svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
933  svgp.end();
934 
935  component.picture = svgPict;
936  // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
937  component.pictureBuffer = 0.0;
938 
939  component.size = QSizeF( sizeOut, sizeOut );
940  component.offset = QPointF( 0.0, 0.0 );
941 
942  // rotate about origin center of SVG
943  QgsScopedQPainterState painterState( p );
944  context.setPainterFlagsUsingContext( p );
945 
946  p->translate( component.center.x(), component.center.y() );
947  p->rotate( component.rotation );
948  double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
949  double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
950  p->translate( QPointF( xoff, yoff ) );
951  p->rotate( component.rotationOffset );
952  p->translate( -sizeOut / 2, sizeOut / 2 );
953 
954  drawShadow( context, component, format );
955  }
956  renderedSymbol.reset( );
957 
959  renderedSymbol.reset( new QgsMarkerSymbol( QgsSymbolLayerList() << symL ) );
960  }
961  else
962  {
963  renderedSymbol.reset( background.markerSymbol()->clone() );
964  renderedSymbol->setSize( sizeOut );
965  renderedSymbol->setSizeUnit( QgsUnitTypes::RenderPixels );
966  }
967 
968  renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
969 
970  // draw the actual symbol
971  QgsScopedQPainterState painterState( p );
972  context.setPainterFlagsUsingContext( p );
973 
974  if ( context.useAdvancedEffects() )
975  {
976  p->setCompositionMode( background.blendMode() );
977  }
978  p->translate( component.center.x(), component.center.y() );
979  p->rotate( component.rotation );
980  double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
981  double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
982  p->translate( QPointF( xoff, yoff ) );
983  p->rotate( component.rotationOffset );
984 
985  const QgsFeature f = context.expressionContext().feature();
986  renderedSymbol->startRender( context, context.expressionContext().fields() );
987  renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
988  renderedSymbol->stopRender( context );
989  p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
990 
991  break;
992  }
993 
998  {
999  double w = component.size.width();
1000  double h = component.size.height();
1001 
1002  if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1003  {
1004  w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1005  background.sizeMapUnitScale() );
1006  h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1007  background.sizeMapUnitScale() );
1008  }
1009  else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1010  {
1011  if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1012  {
1013  if ( w > h )
1014  h = w;
1015  else if ( h > w )
1016  w = h;
1017  }
1018  else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1019  {
1020  // start with label bound by circle
1021  h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1022  w = h;
1023  }
1024  else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1025  {
1026  // start with label bound by ellipse
1027  h = h * M_SQRT1_2 * 2;
1028  w = w * M_SQRT1_2 * 2;
1029  }
1030 
1031  double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(),
1032  background.sizeMapUnitScale() );
1033  double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(),
1034  background.sizeMapUnitScale() );
1035 
1036  w += bufferWidth * 2;
1037  h += bufferHeight * 2;
1038  }
1039 
1040  // offsets match those of symbology: -x = left, -y = up
1041  QRectF rect( -w / 2.0, - h / 2.0, w, h );
1042 
1043  if ( rect.isNull() )
1044  return;
1045 
1046  QgsScopedQPainterState painterState( p );
1047  context.setPainterFlagsUsingContext( p );
1048 
1049  p->translate( QPointF( component.center.x(), component.center.y() ) );
1050  p->rotate( component.rotation );
1051  double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1052  double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1053  p->translate( QPointF( xoff, yoff ) );
1054  p->rotate( component.rotationOffset );
1055 
1056  QPainterPath path;
1057 
1058  // Paths with curves must be enlarged before conversion to QPolygonF, or
1059  // the curves are approximated too much and appear jaggy
1060  QTransform t = QTransform::fromScale( 10, 10 );
1061  // inverse transform used to scale created polygons back to expected size
1062  QTransform ti = t.inverted();
1063 
1064  if ( background.type() == QgsTextBackgroundSettings::ShapeRectangle
1065  || background.type() == QgsTextBackgroundSettings::ShapeSquare )
1066  {
1067  if ( background.radiiUnit() == QgsUnitTypes::RenderPercentage )
1068  {
1069  path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1070  }
1071  else
1072  {
1073  const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1074  const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1075  path.addRoundedRect( rect, xRadius, yRadius );
1076  }
1077  }
1078  else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse
1079  || background.type() == QgsTextBackgroundSettings::ShapeCircle )
1080  {
1081  path.addEllipse( rect );
1082  }
1083  QPolygonF tempPolygon = path.toFillPolygon( t );
1084  QPolygonF polygon = ti.map( tempPolygon );
1085  QPicture shapePict;
1086  QPainter *oldp = context.painter();
1087  QPainter shapep;
1088 
1089  shapep.begin( &shapePict );
1090  context.setPainter( &shapep );
1091 
1092  std::unique_ptr< QgsFillSymbol > renderedSymbol;
1093  renderedSymbol.reset( background.fillSymbol()->clone() );
1094  renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1095 
1096  const QgsFeature f = context.expressionContext().feature();
1097  renderedSymbol->startRender( context, context.expressionContext().fields() );
1098  renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1099  renderedSymbol->stopRender( context );
1100 
1101  shapep.end();
1102  context.setPainter( oldp );
1103 
1104  if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowShape )
1105  {
1106  component.picture = shapePict;
1107  component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1108 
1109  component.size = rect.size();
1110  component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1111  drawShadow( context, component, format );
1112  }
1113 
1114  if ( context.useAdvancedEffects() )
1115  {
1116  p->setCompositionMode( background.blendMode() );
1117  }
1118 
1119  // scale for any print output or image saving @ specific dpi
1120  p->scale( component.dpiRatio, component.dpiRatio );
1121  _fixQPictureDPI( p );
1122  p->drawPicture( 0, 0, shapePict );
1123  p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1124  break;
1125  }
1126  }
1127 
1128  if ( tmpEffect )
1129  {
1130  tmpEffect->end( context );
1131  context.setPainter( prevP );
1132  }
1133 }
1134 
1135 void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1136 {
1137  QgsTextShadowSettings shadow = format.shadow();
1138 
1139  // incoming component sizes should be multiplied by rasterCompressFactor, as
1140  // this allows shadows to be created at paint device dpi (e.g. high resolution),
1141  // then scale device painter by 1.0 / rasterCompressFactor for output
1142 
1143  QPainter *p = context.painter();
1144  double componentWidth = component.size.width(), componentHeight = component.size.height();
1145  double xOffset = component.offset.x(), yOffset = component.offset.y();
1146  double pictbuffer = component.pictureBuffer;
1147 
1148  // generate pixmap representation of label component drawing
1149  bool mapUnits = shadow.blurRadiusUnit() == QgsUnitTypes::RenderMapUnits;
1150  double radius = context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1151  radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1152  radius = static_cast< int >( radius + 0.5 ); //NOLINT
1153 
1154  // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1155  // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1156  double blurBufferClippingScale = 3.75;
1157  int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1158 
1159  QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1160  componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ),
1161  QImage::Format_ARGB32_Premultiplied );
1162 
1163  // TODO: add labeling gui option to not show any shadows under/over a certain size
1164  // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1165  int minBlurImgSize = 1;
1166  // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1167  // 4 x QgsSvgCache limit for output to print/image at higher dpi
1168  // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1169  int maxBlurImgSize = 40000;
1170  if ( blurImg.isNull()
1171  || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize )
1172  || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1173  return;
1174 
1175  blurImg.fill( QColor( Qt::transparent ).rgba() );
1176  QPainter pictp;
1177  if ( !pictp.begin( &blurImg ) )
1178  return;
1179  pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1180  QPointF imgOffset( blurbuffer + pictbuffer + xOffset,
1181  blurbuffer + pictbuffer + componentHeight + yOffset );
1182 
1183  pictp.drawPicture( imgOffset,
1184  component.picture );
1185 
1186  // overlay shadow color
1187  pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1188  pictp.fillRect( blurImg.rect(), shadow.color() );
1189  pictp.end();
1190 
1191  // blur the QImage in-place
1192  if ( shadow.blurRadius() > 0.0 && radius > 0 )
1193  {
1194  QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1195  }
1196 
1197 #if 0
1198  // debug rect for QImage shadow registration and clipping visualization
1199  QPainter picti;
1200  picti.begin( &blurImg );
1201  picti.setBrush( Qt::Dense7Pattern );
1202  QPen imgPen( QColor( 0, 0, 255, 255 ) );
1203  imgPen.setWidth( 1 );
1204  picti.setPen( imgPen );
1205  picti.setOpacity( 0.1 );
1206  picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1207  picti.end();
1208 #endif
1209 
1210  double offsetDist = context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1211  double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1212  if ( shadow.offsetGlobal() )
1213  {
1214  // TODO: check for differences in rotation origin and cw/ccw direction,
1215  // when this shadow function is used for something other than labels
1216 
1217  // it's 0-->cw-->360 for labels
1218  //QgsDebugMsgLevel( QStringLiteral( "Shadow aggregated label rotation (degrees): %1" ).arg( component.rotation() + component.rotationOffset() ), 4 );
1219  angleRad -= ( component.rotation * M_PI / 180 + component.rotationOffset * M_PI / 180 );
1220  }
1221 
1222  QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ),
1223  -offsetDist * std::sin( angleRad + M_PI_2 ) );
1224 
1225  p->save();
1226  p->setRenderHint( QPainter::SmoothPixmapTransform );
1227  context.setPainterFlagsUsingContext( p );
1228  if ( context.useAdvancedEffects() )
1229  {
1230  p->setCompositionMode( shadow.blendMode() );
1231  }
1232  p->setOpacity( shadow.opacity() );
1233 
1234  double scale = shadow.scale() / 100.0;
1235  // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1236  p->scale( scale, scale );
1237  if ( component.useOrigin )
1238  {
1239  p->translate( component.origin.x(), component.origin.y() );
1240  }
1241  p->translate( transPt );
1242  p->translate( -imgOffset.x(),
1243  -imgOffset.y() );
1244  p->drawImage( 0, 0, blurImg );
1245  p->restore();
1246 
1247  // debug rects
1248 #if 0
1249  // draw debug rect for QImage painting registration
1250  p->save();
1251  p->setBrush( Qt::NoBrush );
1252  QPen imgPen( QColor( 255, 0, 0, 10 ) );
1253  imgPen.setWidth( 2 );
1254  imgPen.setStyle( Qt::DashLine );
1255  p->setPen( imgPen );
1256  p->scale( scale, scale );
1257  if ( component.useOrigin() )
1258  {
1259  p->translate( component.origin().x(), component.origin().y() );
1260  }
1261  p->translate( transPt );
1262  p->translate( -imgOffset.x(),
1263  -imgOffset.y() );
1264  p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1265  p->restore();
1266 
1267  // draw debug rect for passed in component dimensions
1268  p->save();
1269  p->setBrush( Qt::NoBrush );
1270  QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1271  componentRectPen.setWidth( 1 );
1272  if ( component.useOrigin() )
1273  {
1274  p->translate( component.origin().x(), component.origin().y() );
1275  }
1276  p->setPen( componentRectPen );
1277  p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1278  p->restore();
1279 #endif
1280 }
1281 
1282 
1283 void QgsTextRenderer::drawTextInternal( TextPart drawType,
1284  QgsRenderContext &context,
1285  const QgsTextFormat &format,
1286  const Component &component,
1287  const QgsTextDocument &document,
1288  const QFontMetricsF *fontMetrics,
1289  HAlignment alignment, VAlignment vAlignment, DrawMode mode )
1290 {
1291  if ( !context.painter() )
1292  {
1293  return;
1294  }
1295 
1296  double fontScale = 1.0;
1297  std::unique_ptr< QFontMetricsF > tmpMetrics;
1298  if ( !fontMetrics )
1299  {
1301 
1302  std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1303  if ( mode == Label )
1304  {
1305  // label size has already been calculated using any symbology reference scale factor -- we need
1306  // to temporarily remove the reference scale here or we'll be applying the scaling twice
1307  referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1308  }
1309 
1310  bool isNullSize = false;
1311  const QFont f = format.scaledFont( context, fontScale, &isNullSize );
1312  if ( isNullSize )
1313  return;
1314 
1315  tmpMetrics = std::make_unique< QFontMetricsF >( f );
1316  fontMetrics = tmpMetrics.get();
1317 
1318  referenceScaleOverride.reset();
1319  }
1320 
1321  double rotation = 0;
1322  const QgsTextFormat::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotation );
1323  switch ( orientation )
1324  {
1326  {
1327  drawTextInternalHorizontal( context, format, drawType, mode, component, document, fontScale, fontMetrics, alignment, vAlignment, rotation );
1328  break;
1329  }
1330 
1333  {
1334  drawTextInternalVertical( context, format, drawType, mode, component, document, fontScale, fontMetrics, alignment, vAlignment, rotation );
1335  break;
1336  }
1337  }
1338 }
1339 
1340 QgsTextFormat::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotation )
1341 {
1342  rotation = -component.rotation * 180 / M_PI;
1343 
1344  switch ( format.orientation() )
1345  {
1347  {
1348  // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1349  if ( rotation >= -315 && rotation < -90 )
1350  {
1351  rotation -= 90;
1353  }
1354  else if ( rotation >= -90 && rotation < -45 )
1355  {
1356  rotation += 90;
1358  }
1359 
1361  }
1362 
1365  return format.orientation();
1366  }
1368 }
1369 
1370 void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1371 {
1372  const QString blockText = block.toPlainText();
1373  QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1374  finder.toStart();
1375  int wordBoundaries = 0;
1376  while ( finder.toNextBoundary() != -1 )
1377  {
1378  if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1379  wordBoundaries++;
1380  }
1381 
1382  if ( wordBoundaries > 0 )
1383  {
1384  // word boundaries found => justify by padding word spacing
1385  extraWordSpace = spaceToDistribute / wordBoundaries;
1386  }
1387  else
1388  {
1389  // no word boundaries found => justify by letter spacing
1390  QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1391  finder.toStart();
1392 
1393  int graphemeBoundaries = 0;
1394  while ( finder.toNextBoundary() != -1 )
1395  {
1396  if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1397  graphemeBoundaries++;
1398  }
1399 
1400  if ( graphemeBoundaries > 0 )
1401  {
1402  extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1403  }
1404  }
1405 }
1406 
1407 void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1408 {
1409  const double prevWordSpace = font.wordSpacing();
1410  font.setWordSpacing( prevWordSpace + extraWordSpace );
1411  const double prevLetterSpace = font.letterSpacing();
1412  font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1413 }
1414 
1415 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,
1416  VAlignment vAlignment, double rotation )
1417 {
1418  QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1419  const QStringList textLines = document.toPlainText();
1420 
1421  double labelWidest = 0.0;
1422  switch ( mode )
1423  {
1424  case Label:
1425  case Point:
1426  for ( const QString &line : textLines )
1427  {
1428  double labelWidth = fontMetrics->horizontalAdvance( line ) / fontScale;
1429  if ( labelWidth > labelWidest )
1430  {
1431  labelWidest = labelWidth;
1432  }
1433  }
1434  break;
1435 
1436  case Rect:
1437  labelWidest = component.size.width();
1438  break;
1439  }
1440 
1441  double labelHeight = ( fontMetrics->ascent() + fontMetrics->descent() ) / fontScale; // ignore +1 for baseline
1442  // double labelHighest = labelfm->height() + ( double )(( lines - 1 ) * labelHeight * tmpLyr.multilineHeight );
1443 
1444  // needed to move bottom of text's descender to within bottom edge of label
1445  double ascentOffset = 0.25 * fontMetrics->ascent() / fontScale; // labelfm->descent() is not enough
1446 
1447  int i = 0;
1448 
1449  bool adjustForAlignment = hAlignment != AlignLeft && ( mode != Label || textLines.size() > 1 );
1450 
1451  if ( mode == Rect && vAlignment != AlignTop )
1452  {
1453  std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1454 
1455  const double overallHeight = textHeight( context, format, textLines, Rect );
1456  switch ( vAlignment )
1457  {
1458  case AlignTop:
1459  break;
1460 
1461  case AlignVCenter:
1462  ascentOffset = -( component.size.height() - overallHeight ) * 0.5 + ascentOffset;
1463  break;
1464 
1465  case AlignBottom:
1466  ascentOffset = -( component.size.height() - overallHeight ) + ascentOffset;
1467  break;
1468  }
1469  referenceScaleOverride.reset();
1470  }
1471 
1472  for ( const QString &line : std::as_const( textLines ) )
1473  {
1474  const QgsTextBlock block = document.at( i );
1475 
1476  const bool isFinalLine = i == document.size() - 1;
1477 
1478  QgsScopedQPainterState painterState( context.painter() );
1479  context.setPainterFlagsUsingContext();
1480  context.painter()->translate( component.origin );
1481  if ( !qgsDoubleNear( rotation, 0.0 ) )
1482  context.painter()->rotate( rotation );
1483 
1484  // apply to the mask painter the same transformations
1485  if ( maskPainter )
1486  {
1487  maskPainter->save();
1488  maskPainter->translate( component.origin );
1489  if ( !qgsDoubleNear( rotation, 0.0 ) )
1490  maskPainter->rotate( rotation );
1491  }
1492 
1493  // figure x offset for horizontal alignment of multiple lines
1494  double xMultiLineOffset = 0.0;
1495  double labelWidth = fontMetrics->horizontalAdvance( line ) / fontScale;
1496  double extraWordSpace = 0;
1497  double extraLetterSpace = 0;
1498  if ( adjustForAlignment )
1499  {
1500  double labelWidthDiff = 0;
1501  switch ( hAlignment )
1502  {
1503  case AlignCenter:
1504  labelWidthDiff = ( labelWidest - labelWidth ) * 0.5;
1505  break;
1506 
1507  case AlignRight:
1508  labelWidthDiff = labelWidest - labelWidth;
1509  break;
1510 
1511  case AlignJustify:
1512  if ( !isFinalLine && labelWidest > labelWidth )
1513  {
1514  calculateExtraSpacingForLineJustification( labelWidest - labelWidth, block, extraWordSpace, extraLetterSpace );
1515  }
1516  break;
1517 
1518  case AlignLeft:
1519  break;
1520  }
1521 
1522  switch ( mode )
1523  {
1524  case Label:
1525  case Rect:
1526  xMultiLineOffset = labelWidthDiff;
1527  break;
1528 
1529  case Point:
1530  {
1531  switch ( hAlignment )
1532  {
1533  case AlignRight:
1534  xMultiLineOffset = labelWidthDiff - labelWidest;
1535  break;
1536 
1537  case AlignCenter:
1538  xMultiLineOffset = labelWidthDiff - labelWidest / 2.0;
1539  break;
1540 
1541  case AlignLeft:
1542  case AlignJustify:
1543  break;
1544  }
1545  }
1546  break;
1547  }
1548  }
1549 
1550  double yMultiLineOffset = ascentOffset;
1551  switch ( mode )
1552  {
1553  case Label:
1554  // rendering labels needs special handling - in this case text should be
1555  // drawn with the bottom left corner coinciding with origin, vs top left
1556  // for standard text rendering. Line height is also slightly different.
1557  yMultiLineOffset = - ascentOffset - ( textLines.size() - 1 - i ) * labelHeight * format.lineHeight();
1558  break;
1559 
1560  case Rect:
1561  // standard rendering - designed to exactly replicate QPainter's drawText method
1562  yMultiLineOffset = - ascentOffset + labelHeight - 1 /*baseline*/ + format.lineHeight() * fontMetrics->lineSpacing() * i / fontScale;
1563  break;
1564 
1565  case Point:
1566  // standard rendering - designed to exactly replicate QPainter's drawText rect method
1567  yMultiLineOffset = 0 - ( textLines.size() - 1 - i ) * fontMetrics->lineSpacing() * format.lineHeight() / fontScale;
1568  break;
1569 
1570  }
1571 
1572  context.painter()->translate( QPointF( xMultiLineOffset, yMultiLineOffset ) );
1573  if ( maskPainter )
1574  maskPainter->translate( QPointF( xMultiLineOffset, yMultiLineOffset ) );
1575 
1576  Component subComponent;
1577  subComponent.block = block;
1578  subComponent.size = QSizeF( labelWidth, labelHeight );
1579  subComponent.offset = QPointF( 0.0, -ascentOffset );
1580  subComponent.rotation = -component.rotation * 180 / M_PI;
1581  subComponent.rotationOffset = 0.0;
1582  subComponent.extraWordSpacing = extraWordSpace * fontScale;
1583  subComponent.extraLetterSpacing = extraLetterSpace * fontScale;
1584 
1585  // draw the mask below the text (for preview)
1586  if ( format.mask().enabled() )
1587  {
1588  QgsTextRenderer::drawMask( context, subComponent, format, mode );
1589  }
1590 
1591  if ( drawType == QgsTextRenderer::Buffer )
1592  {
1593  QgsTextRenderer::drawBuffer( context, subComponent, format, mode );
1594  }
1595  else
1596  {
1597  // store text's drawing in QPicture for drop shadow call
1598  QPicture textPict;
1599  QPainter textp;
1600  textp.begin( &textPict );
1601  textp.setPen( Qt::NoPen );
1602 
1603  std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1604  if ( mode == Label )
1605  {
1606  // label size has already been calculated using any symbology reference scale factor -- we need
1607  // to temporarily remove the reference scale here or we'll be applying the scaling twice
1608  referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1609  }
1610  bool isNullSize = false;
1611  const QFont font = format.scaledFont( context, fontScale, &isNullSize );
1612  referenceScaleOverride.reset();
1613 
1614  if ( !isNullSize )
1615  {
1616  textp.scale( 1 / fontScale, 1 / fontScale );
1617 
1618  double xOffset = 0;
1619  for ( const QgsTextFragment &fragment : block )
1620  {
1621  // draw text, QPainterPath method
1622  QPainterPath path;
1623  path.setFillRule( Qt::WindingFill );
1624 
1625  QFont fragmentFont = font;
1626  fragment.characterFormat().updateFontForFormat( fragmentFont, fontScale );
1627 
1628  if ( extraWordSpace || extraLetterSpace )
1629  applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1630 
1631  path.addText( xOffset, 0, fragmentFont, fragment.text() );
1632 
1633  QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1634  textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1635  textp.setBrush( textColor );
1636  textp.drawPath( path );
1637 
1638  xOffset += fragment.horizontalAdvance( fragmentFont, true );
1639  }
1640  textp.end();
1641  }
1642 
1643  if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
1644  {
1645  subComponent.picture = textPict;
1646  subComponent.pictureBuffer = 0.0; // no pen width to deal with
1647  subComponent.origin = QPointF( 0.0, 0.0 );
1648 
1649  QgsTextRenderer::drawShadow( context, subComponent, format );
1650  }
1651 
1652  // paint the text
1653  if ( context.useAdvancedEffects() )
1654  {
1655  context.painter()->setCompositionMode( format.blendMode() );
1656  }
1657 
1658  // scale for any print output or image saving @ specific dpi
1659  context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
1660 
1661  switch ( context.textRenderFormat() )
1662  {
1663  case Qgis::TextRenderFormat::AlwaysOutlines:
1664  {
1665  // draw outlined text
1666  _fixQPictureDPI( context.painter() );
1667  context.painter()->drawPicture( 0, 0, textPict );
1668  break;
1669  }
1670 
1671  case Qgis::TextRenderFormat::AlwaysText:
1672  {
1673  double xOffset = 0;
1674  for ( const QgsTextFragment &fragment : block )
1675  {
1676  QFont fragmentFont = font;
1677  fragment.characterFormat().updateFontForFormat( fragmentFont, fontScale );
1678 
1679  if ( extraWordSpace || extraLetterSpace )
1680  applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1681 
1682  QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1683  textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1684 
1685  context.painter()->setPen( textColor );
1686  context.painter()->setFont( fragmentFont );
1687  context.painter()->setRenderHint( QPainter::TextAntialiasing );
1688 
1689  context.painter()->scale( 1 / fontScale, 1 / fontScale );
1690  context.painter()->drawText( xOffset, 0, fragment.text() );
1691  context.painter()->scale( fontScale, fontScale );
1692 
1693  xOffset += fragment.horizontalAdvance( fragmentFont, true, fontScale );
1694  }
1695  }
1696  }
1697  }
1698  if ( maskPainter )
1699  maskPainter->restore();
1700  i++;
1701  }
1702 }
1703 
1704 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 )
1705 {
1706  QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
1707  const QStringList textLines = document.toPlainText();
1708 
1709  std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1710  if ( mode == Label )
1711  {
1712  // label size has already been calculated using any symbology reference scale factor -- we need
1713  // to temporarily remove the reference scale here or we'll be applying the scaling twice
1714  referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1715  }
1716 
1717  bool isNullSize = false;
1718  const QFont font = format.scaledFont( context, fontScale, &isNullSize );
1719  if ( isNullSize )
1720  return;
1721 
1722  referenceScaleOverride.reset();
1723 
1724  double letterSpacing = font.letterSpacing() / fontScale;
1725 
1726  double labelWidth = fontMetrics->maxWidth() / fontScale; // label width represents the width of one line of a multi-line label
1727  double actualLabelWidest = labelWidth + ( textLines.size() - 1 ) * labelWidth * format.lineHeight();
1728  double labelWidest = 0.0;
1729  switch ( mode )
1730  {
1731  case Label:
1732  case Point:
1733  labelWidest = actualLabelWidest;
1734  break;
1735 
1736  case Rect:
1737  labelWidest = component.size.width();
1738  break;
1739  }
1740 
1741  int maxLineLength = 0;
1742  for ( const QString &line : std::as_const( textLines ) )
1743  {
1744  maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
1745  }
1746  double actualLabelHeight = fontMetrics->ascent() / fontScale + ( fontMetrics->ascent() / fontScale + letterSpacing ) * ( maxLineLength - 1 );
1747  double ascentOffset = fontMetrics->ascent() / fontScale;
1748 
1749  int i = 0;
1750 
1751  bool adjustForAlignment = hAlignment != AlignLeft && ( mode != Label || textLines.size() > 1 );
1752 
1753  for ( const QgsTextBlock &block : document )
1754  {
1755  QgsScopedQPainterState painterState( context.painter() );
1756  context.setPainterFlagsUsingContext();
1757 
1758  context.painter()->translate( component.origin );
1759  if ( !qgsDoubleNear( rotation, 0.0 ) )
1760  context.painter()->rotate( rotation );
1761 
1762  // apply to the mask painter the same transformations
1763  if ( maskPainter )
1764  {
1765  maskPainter->save();
1766  maskPainter->translate( component.origin );
1767  if ( !qgsDoubleNear( rotation, 0.0 ) )
1768  maskPainter->rotate( rotation );
1769  }
1770 
1771  // figure x offset of multiple lines
1772  double xOffset = actualLabelWidest - labelWidth - ( i * labelWidth * format.lineHeight() );
1773  if ( adjustForAlignment )
1774  {
1775  double labelWidthDiff = 0;
1776  switch ( hAlignment )
1777  {
1778  case AlignCenter:
1779  labelWidthDiff = ( labelWidest - actualLabelWidest ) * 0.5;
1780  break;
1781 
1782  case AlignRight:
1783  labelWidthDiff = labelWidest - actualLabelWidest;
1784  break;
1785 
1786  case AlignLeft:
1787  case AlignJustify:
1788  break;
1789  }
1790 
1791  switch ( mode )
1792  {
1793  case Label:
1794  case Rect:
1795  xOffset += labelWidthDiff;
1796  break;
1797 
1798  case Point:
1799  break;
1800  }
1801  }
1802 
1803  double yOffset = 0.0;
1804  switch ( mode )
1805  {
1806  case Label:
1808  {
1809  if ( rotation >= -405 && rotation < -180 )
1810  {
1811  yOffset = ascentOffset;
1812  }
1813  else if ( rotation >= 0 && rotation < 45 )
1814  {
1815  xOffset -= actualLabelWidest;
1816  yOffset = -actualLabelHeight + ascentOffset + fontMetrics->descent() / fontScale;
1817  }
1818  }
1819  else
1820  {
1821  yOffset = -actualLabelHeight + ascentOffset;
1822  }
1823  break;
1824 
1825  case Point:
1826  yOffset = -actualLabelHeight + ascentOffset;
1827  break;
1828 
1829  case Rect:
1830  yOffset = ascentOffset;
1831  break;
1832  }
1833 
1834  context.painter()->translate( QPointF( xOffset, yOffset ) );
1835 
1836  double fragmentYOffset = 0;
1837  for ( const QgsTextFragment &fragment : block )
1838  {
1839  // apply some character replacement to draw symbols in vertical presentation
1840  const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
1841 
1842  QFont fragmentFont( font );
1843  fragment.characterFormat().updateFontForFormat( fragmentFont, fontScale );
1844 
1845  QFontMetricsF fragmentMetrics( fragmentFont );
1846 
1847  double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
1848 
1849  Component subComponent;
1850  subComponent.block = QgsTextBlock( fragment );
1851  subComponent.size = QSizeF( labelWidth, labelHeight );
1852  subComponent.offset = QPointF( 0.0, fragmentYOffset );
1853  subComponent.rotation = -component.rotation * 180 / M_PI;
1854  subComponent.rotationOffset = 0.0;
1855 
1856  // draw the mask below the text (for preview)
1857  if ( format.mask().enabled() )
1858  {
1859  // WARNING: totally broken! (has been since mask was introduced)
1860 #if 0
1861  QgsTextRenderer::drawMask( context, subComponent, format );
1862 #endif
1863  }
1864 
1865  if ( drawType == QgsTextRenderer::Buffer )
1866  {
1867  fragmentYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, mode );
1868  }
1869  else
1870  {
1871  // draw text, QPainterPath method
1872  QPainterPath path;
1873  path.setFillRule( Qt::WindingFill );
1874  const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
1875  double partYOffset = 0.0;
1876  for ( const auto &part : parts )
1877  {
1878  double partXOffset = ( labelWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
1879  path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
1880  partYOffset += fragmentMetrics.ascent() / fontScale + letterSpacing;
1881  }
1882 
1883  // store text's drawing in QPicture for drop shadow call
1884  QPicture textPict;
1885  QPainter textp;
1886  textp.begin( &textPict );
1887  textp.setPen( Qt::NoPen );
1888  QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1889  textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1890  textp.setBrush( textColor );
1891  textp.scale( 1 / fontScale, 1 / fontScale );
1892  textp.drawPath( path );
1893  // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
1894  // e.g. some capitalization options, but not others
1895  //textp.setFont( tmpLyr.textFont );
1896  //textp.setPen( tmpLyr.textColor );
1897  //textp.drawText( 0, 0, component.text() );
1898  textp.end();
1899 
1900  if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
1901  {
1902  subComponent.picture = textPict;
1903  subComponent.pictureBuffer = 0.0; // no pen width to deal with
1904  subComponent.origin = QPointF( 0.0, fragmentYOffset );
1905  const double prevY = subComponent.offset.y();
1906  subComponent.offset = QPointF( 0, -labelHeight );
1907  subComponent.useOrigin = true;
1908  QgsTextRenderer::drawShadow( context, subComponent, format );
1909  subComponent.useOrigin = false;
1910  subComponent.offset = QPointF( 0, prevY );
1911  }
1912 
1913  // paint the text
1914  if ( context.useAdvancedEffects() )
1915  {
1916  context.painter()->setCompositionMode( format.blendMode() );
1917  }
1918 
1919  // scale for any print output or image saving @ specific dpi
1920  context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
1921 
1922  switch ( context.textRenderFormat() )
1923  {
1924  case Qgis::TextRenderFormat::AlwaysOutlines:
1925  {
1926  // draw outlined text
1927  _fixQPictureDPI( context.painter() );
1928  context.painter()->drawPicture( 0, fragmentYOffset, textPict );
1929  fragmentYOffset += partYOffset;
1930  break;
1931  }
1932 
1933  case Qgis::TextRenderFormat::AlwaysText:
1934  {
1935  context.painter()->setFont( fragmentFont );
1936  context.painter()->setPen( textColor );
1937  context.painter()->setRenderHint( QPainter::TextAntialiasing );
1938 
1939  double partYOffset = 0.0;
1940  for ( const QString &part : parts )
1941  {
1942  double partXOffset = ( labelWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
1943  context.painter()->scale( 1 / fontScale, 1 / fontScale );
1944  context.painter()->drawText( partXOffset * fontScale, ( fragmentYOffset + partYOffset ) * fontScale, part );
1945  context.painter()->scale( fontScale, fontScale );
1946  partYOffset += fragmentMetrics.ascent() / fontScale + letterSpacing;
1947  }
1948  fragmentYOffset += partYOffset;
1949  }
1950  }
1951  }
1952  }
1953 
1954  if ( maskPainter )
1955  maskPainter->restore();
1956  i++;
1957  }
1958 }
1959 
@ 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
double lineHeight() const
Returns the line height for 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.
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:1757
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1234
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()