QGIS API Documentation 4.3.0-Master (0ad0cdb9438)
Loading...
Searching...
No Matches
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
18#include <memory>
19#include <optional>
20
21#include "qgsapplication.h"
22#include "qgsfillsymbol.h"
23#include "qgsgeos.h"
24#include "qgsimagecache.h"
25#include "qgsmarkersymbol.h"
27#include "qgspainteffect.h"
28#include "qgspainterswapper.h"
29#include "qgspainting.h"
30#include "qgspallabeling.h"
31#include "qgssymbollayerutils.h"
32#include "qgstextdocument.h"
34#include "qgstextformat.h"
35#include "qgstextfragment.h"
36#include "qgstextmetrics.h"
38#include "qgsunittypes.h"
39
40#include <QString>
41#include <QTextBoundaryFinder>
42
43using namespace Qt::StringLiterals;
44
46{
47 if ( alignment & Qt::AlignLeft )
49 else if ( alignment & Qt::AlignRight )
51 else if ( alignment & Qt::AlignHCenter )
53 else if ( alignment & Qt::AlignJustify )
55
56 // not supported?
58}
59
61{
62 if ( alignment & Qt::AlignTop )
64 else if ( alignment & Qt::AlignBottom )
66 else if ( alignment & Qt::AlignVCenter )
68 //not supported
69 else if ( alignment & Qt::AlignBaseline )
71
73}
74
75int QgsTextRenderer::sizeToPixel( double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale )
76{
77 return static_cast< int >( c.convertToPainterUnits( size, unit, mapUnitScale ) + 0.5 ); //NOLINT
78}
79
81 const QRectF &rect,
82 double rotation,
84 const QStringList &text,
85 QgsRenderContext &context,
86 const QgsTextFormat &_format,
87 bool,
91)
92{
93 QgsTextFormat lFormat = _format;
94 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use format instead of tmpFormat here, it's const and potentially avoids a detach
95 lFormat.updateDataDefinedProperties( context );
96
97 // DO NOT USE _format in the following code, always use lFormat!!
98 QgsTextDocumentRenderContext documentContext;
99 documentContext.setFlags( flags );
100 documentContext.setMaximumWidth( rect.width() );
101
102 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( text, lFormat );
103
104 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
105 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale, documentContext );
106
107 drawDocument( rect, lFormat, metrics.document(), metrics, context, alignment, vAlignment, rotation, mode, flags );
108}
109
111 const QRectF &rect,
112 const QgsTextFormat &format,
113 const QgsTextDocument &document,
114 const QgsTextDocumentMetrics &metrics,
115 QgsRenderContext &context,
116 Qgis::TextHorizontalAlignment horizontalAlignment,
117 Qgis::TextVerticalAlignment verticalAlignment,
118 double rotation,
121)
122{
123 const QgsTextFormat tmpFormat = updateShadowPosition( format );
124
126 if ( tmpFormat.background().enabled() )
127 {
129 }
130
131 if ( tmpFormat.shadow().enabled() )
132 {
133 components |= Qgis::TextComponent::Shadow;
134 }
135
136 if ( tmpFormat.buffer().enabled() )
137 {
138 components |= Qgis::TextComponent::Buffer;
139 }
140
141 drawParts( rect, rotation, horizontalAlignment, verticalAlignment, document, metrics, context, tmpFormat, components, mode );
142}
143
144void QgsTextRenderer::drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &_format, bool )
145{
146 QgsTextFormat lFormat = _format;
147 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
148 lFormat.updateDataDefinedProperties( context );
149 lFormat = updateShadowPosition( lFormat );
150
151 // DO NOT USE _format in the following code, always use lFormat!!
152 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, lFormat );
153 const double fontScale = calculateScaleFactorForFormat( context, lFormat );
154 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, lFormat, context, fontScale );
155
156 drawDocument( point, lFormat, metrics.document(), metrics, context, alignment, rotation );
157}
158
160 QPointF point,
161 const QgsTextFormat &_format,
162 const QgsTextDocument &document,
163 const QgsTextDocumentMetrics &metrics,
164 QgsRenderContext &context,
166 double rotation,
168)
169{
170 const QgsTextFormat lFormat = updateShadowPosition( _format );
171 // DO NOT USE _format in the following code, always use lFormat!!
172
174 if ( lFormat.background().enabled() )
175 {
177 }
178
179 if ( lFormat.shadow().enabled() )
180 {
181 components |= Qgis::TextComponent::Shadow;
182 }
183
184 if ( lFormat.buffer().enabled() )
185 {
186 components |= Qgis::TextComponent::Buffer;
187 }
188
189 drawParts( point, rotation, alignment, document, metrics, context, lFormat, components, mode );
190}
191
193 const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &_format, double offsetAlongLine, double offsetFromLine, Qgis::CurvedTextFlags flags, Qgis::TextAnchorPoint textAnchor
194)
195{
196 QgsTextFormat lFormat = _format;
197 if ( _format.dataDefinedProperties().hasActiveProperties() ) // note, we use _format instead of tmpFormat here, it's const and potentially avoids a detach
198 lFormat.updateDataDefinedProperties( context );
199 lFormat = updateShadowPosition( lFormat );
200
201 // DO NOT USE _format in the following code, always use lFormat!!
202
203 // todo handle newlines??
204 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( { text }, lFormat );
205
206 drawDocumentOnLine( line, lFormat, document, context, offsetAlongLine, offsetFromLine, flags, textAnchor );
207}
208
210 const QPolygonF &line,
211 const QgsTextFormat &format,
212 const QgsTextDocument &document,
213 QgsRenderContext &context,
214 double offsetAlongLine,
215 double offsetFromLine,
217 Qgis::TextAnchorPoint textAnchor
218)
219{
220 QPolygonF labelBaselineCurve = line;
221 if ( !qgsDoubleNear( offsetFromLine, 0 ) )
222 {
223 std::unique_ptr< QgsLineString > ring( QgsLineString::fromQPolygonF( line ) );
224 QgsGeos geos( ring.get() );
225 std::unique_ptr< QgsLineString > offsetCurve( dynamic_cast< QgsLineString * >( geos.offsetCurve( offsetFromLine, 4, Qgis::JoinStyle::Round, 2 ) ) );
226 if ( !offsetCurve )
227 return;
228
229#if GEOS_VERSION_NUM < GEOS_COMPUTE_VERSION( 3, 11, 0 )
230 if ( offsetFromLine < 0 )
231 {
232 // geos < 3.11 reverses the direction of offset curves with negative distances -- we don't want that!
233 std::unique_ptr< QgsLineString > reversed( offsetCurve->reversed() );
234 if ( !reversed )
235 return;
236
237 offsetCurve = std::move( reversed );
238 }
239#endif
240
241 labelBaselineCurve = offsetCurve->asQPolygonF();
242 }
243
244 const double fontScale = calculateScaleFactorForFormat( context, format );
245
246 const QFont baseFont = format.scaledFont( context, fontScale );
247 const double letterSpacing = baseFont.letterSpacing() / fontScale;
248 const double wordSpacing = baseFont.wordSpacing() / fontScale;
249
250 QStringList graphemes;
251 QVector< QgsTextCharacterFormat > graphemeFormats;
252 QVector< QgsTextDocumentMetrics > graphemeMetrics;
253
254 for ( const QgsTextBlock &block : std::as_const( document ) )
255 {
256 for ( const QgsTextFragment &fragment : block )
257 {
258 const QStringList fragmentGraphemes = QgsPalLabeling::splitToGraphemes( fragment.text() );
259 for ( const QString &grapheme : fragmentGraphemes )
260 {
261 graphemes.append( grapheme );
262 graphemeFormats.append( fragment.characterFormat() );
263
264 QgsTextDocument document;
265 document.append( QgsTextBlock( QgsTextFragment( grapheme, fragment.characterFormat() ) ) );
266
267 graphemeMetrics.append( QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale ) );
268 }
269 }
270 }
271
272 QVector< double > characterWidths( graphemes.count() );
273 QVector< double > characterHeights( graphemes.count() );
274 QVector< double > characterDescents( graphemes.count() );
275 QFont previousNonSuperSubScriptFont;
276
277 for ( int i = 0; i < graphemes.count(); i++ )
278 {
279 // reconstruct how Qt creates word spacing, then adjust per individual stored character
280 // this will allow the text renderer to create each candidate width = character width + correct spacing
281
282 double graphemeFirstCharHorizontalAdvanceWithLetterSpacing = 0;
283 double graphemeFirstCharHorizontalAdvance = 0;
284 double graphemeHorizontalAdvance = 0;
285 double characterDescent = 0;
286 double characterHeight = 0;
287 const QgsTextCharacterFormat *graphemeFormat = &graphemeFormats[i];
288
289 QFont graphemeFont = baseFont;
290 graphemeFormat->updateFontForFormat( graphemeFont, context, fontScale );
291
292 if ( i == 0 )
293 previousNonSuperSubScriptFont = graphemeFont;
294
295 if ( graphemeFormat->hasVerticalAlignmentSet() )
296 {
297 switch ( graphemeFormat->verticalAlignment() )
298 {
300 previousNonSuperSubScriptFont = graphemeFont;
301 break;
302
305 {
306 if ( graphemeFormat->fontPointSize() < 0 )
307 {
308 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
309 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
310 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
311 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
312 // which we should respect
313 graphemeFont.setPixelSize( static_cast< int >( std::round( graphemeFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
314 }
315 break;
316 }
317 }
318 }
319 else
320 {
321 previousNonSuperSubScriptFont = graphemeFont;
322 }
323
324 const QFontMetricsF graphemeFontMetrics( graphemeFont );
325 graphemeFirstCharHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i].at( 0 ) ) ) / fontScale;
326 graphemeFirstCharHorizontalAdvanceWithLetterSpacing = graphemeFontMetrics.horizontalAdvance( graphemes[i].at( 0 ) ) / fontScale + letterSpacing;
327 graphemeHorizontalAdvance = graphemeFontMetrics.horizontalAdvance( QString( graphemes[i] ) ) / fontScale;
328 characterDescent = graphemeFontMetrics.descent() / fontScale;
329 characterHeight = graphemeFontMetrics.height() / fontScale;
330
331 qreal wordSpaceFix = qreal( 0.0 );
332 if ( graphemes[i] == " "_L1 )
333 {
334 // word spacing only gets added once at end of consecutive run of spaces, see QTextEngine::shapeText()
335 int nxt = i + 1;
336 wordSpaceFix = ( nxt < graphemes.count() && graphemes[nxt] != " "_L1 ) ? wordSpacing : qreal( 0.0 );
337 }
338
339 // this workaround only works for clusters with a single character. Not sure how it should be handled
340 // with multi-character clusters.
341 if ( graphemes[i].length() == 1 && !qgsDoubleNear( graphemeFirstCharHorizontalAdvance, graphemeFirstCharHorizontalAdvanceWithLetterSpacing ) )
342 {
343 // word spacing applied when it shouldn't be
344 wordSpaceFix -= wordSpacing;
345 }
346
347 const double charWidth = graphemeHorizontalAdvance + wordSpaceFix;
348 characterWidths[i] = charWidth;
349 characterHeights[i] = characterHeight;
350 characterDescents[i] = characterDescent;
351 }
352
353 QgsPrecalculatedTextMetrics metrics( graphemes, std::move( characterWidths ), std::move( characterHeights ), std::move( characterDescents ) );
354 metrics.setGraphemeFormats( graphemeFormats );
355
356 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement
357 = QgsTextRendererUtils::generateCurvedTextPlacement( metrics, labelBaselineCurve, offsetAlongLine, QgsTextRendererUtils::RespectPainterOrientation, -1, -1, flags, textAnchor );
358
359 if ( placement->graphemePlacement.empty() )
360 return;
361
362 // We may have deliberately skipped over some graphemes during curved text placement (such as zero-width graphemes).
363 // So we need to use a hash of the original grapheme index to place generated components in, as there may accordingly
364 // be graphemes which don't result in components, and we can't just blindly assume the component array position
365 // will match the original grapheme index
366 QHash< int, QgsTextRenderer::Component > components;
367 components.reserve( placement->graphemePlacement.size() );
368 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
369 {
370 QgsTextRenderer::Component component;
371 component.origin = QPointF( grapheme.x, grapheme.y );
372 component.rotationRadians = -grapheme.angle;
373
374 QgsTextDocumentMetrics &metrics = graphemeMetrics[grapheme.graphemeIndex];
375 const double verticalOffset = metrics.fragmentVerticalOffset( 0, 0, Qgis::TextLayoutMode::Point );
376 if ( !qgsDoubleNear( verticalOffset, 0 ) )
377 {
378 component.origin.rx() += verticalOffset * std::cos( grapheme.angle + M_PI_2 );
379 component.origin.ry() += verticalOffset * std::sin( grapheme.angle + M_PI_2 );
380 }
381
382 components.insert( grapheme.graphemeIndex, component );
383 }
384
385 if ( format.background().enabled() )
386 {
387 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
388 {
389 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
390 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex];
391 drawBackground( context, component, format, metrics, Qgis::TextLayoutMode::Point );
392 }
393 }
394
395 if ( format.buffer().enabled() )
396 {
397 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
398 {
399 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
400 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex];
401
403 }
404 }
405
406 for ( const QgsTextRendererUtils::CurvedGraphemePlacement &grapheme : std::as_const( placement->graphemePlacement ) )
407 {
408 const QgsTextDocumentMetrics &metrics = graphemeMetrics.at( grapheme.graphemeIndex );
409 const QgsTextRenderer::Component &component = components[grapheme.graphemeIndex];
410
412 }
413}
414
415QgsTextFormat QgsTextRenderer::updateShadowPosition( const QgsTextFormat &format )
416{
418 return format;
419
420 QgsTextFormat tmpFormat = format;
421 if ( tmpFormat.background().enabled() && tmpFormat.background().type() != QgsTextBackgroundSettings::ShapeMarkerSymbol ) // background shadow not compatible with marker symbol backgrounds
422 {
424 }
425 else if ( tmpFormat.buffer().enabled() )
426 {
428 }
429 else
430 {
432 }
433 return tmpFormat;
434}
435
437 const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool
438)
439{
440 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
441 const double fontScale = calculateScaleFactorForFormat( context, format );
442 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
443
444 drawParts( rect, rotation, alignment, Qgis::TextVerticalAlignment::Top, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Rectangle );
445}
446
447void QgsTextRenderer::drawParts(
448 const QRectF &rect,
449 double rotation,
452 const QgsTextDocument &document,
453 const QgsTextDocumentMetrics &metrics,
454 QgsRenderContext &context,
455 const QgsTextFormat &format,
458)
459{
460 if ( !context.painter() )
461 {
462 return;
463 }
464
465 Component component;
466 component.dpiRatio = 1.0;
467 component.origin = rect.topLeft();
468 component.rotationRadians = rotation;
469 component.size = rect.size();
470 component.hAlign = alignment;
471
472 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
473 {
474 Component backgroundComponent = component;
475 if ( !qgsDoubleNear( rotation, 0.0 ) )
476 {
477 // get rotated label's center point
478
479 double xc = rect.width() / 2.0;
480 double yc = rect.height() / 2.0;
481
482 double angle = -rotation;
483 double xd = xc * std::cos( angle ) - yc * std::sin( angle );
484 double yd = xc * std::sin( angle ) + yc * std::cos( angle );
485
486 backgroundComponent.center = QPointF( backgroundComponent.origin.x() + xd, backgroundComponent.origin.y() + yd );
487 }
488 else
489 {
490 backgroundComponent.center = rect.center();
491 }
492
493 switch ( vAlignment )
494 {
496 break;
498 backgroundComponent.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() ) / 2;
499 break;
501 backgroundComponent.origin.ry() += ( rect.height() - metrics.documentSize( mode, format.orientation() ).height() );
502 break;
503 }
504
505 QgsTextRenderer::drawBackground( context, backgroundComponent, format, metrics, Qgis::TextLayoutMode::Rectangle );
506 }
507
508 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
509 {
510 return;
511 }
512
514 {
515 drawTextInternal( parts, context, format, component, document, metrics, alignment, vAlignment, mode );
516 }
517}
518
520 QPointF origin, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool
521)
522{
523 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( textLines, format );
524 const double fontScale = calculateScaleFactorForFormat( context, format );
525 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, fontScale );
526
527 drawParts( origin, rotation, alignment, metrics.document(), metrics, context, format, part, Qgis::TextLayoutMode::Point );
528}
529
530void QgsTextRenderer::drawParts(
531 QPointF origin,
532 double rotation,
534 const QgsTextDocument &document,
535 const QgsTextDocumentMetrics &metrics,
536 QgsRenderContext &context,
537 const QgsTextFormat &format,
540)
541{
542 if ( !context.painter() )
543 {
544 return;
545 }
546
547 Component component;
548 component.dpiRatio = 1.0;
549 component.origin = origin;
550 component.rotationRadians = rotation;
551 component.hAlign = alignment;
552
553 if ( ( parts & Qgis::TextComponent::Background ) && format.background().enabled() )
554 {
555 QgsTextRenderer::drawBackground( context, component, format, metrics, mode );
556 }
557
558 if ( parts == Qgis::TextComponents( Qgis::TextComponent::Buffer ) && !format.buffer().enabled() )
559 {
560 return;
561 }
562
564 {
565 drawTextInternal( parts, context, format, component, document, metrics, alignment, Qgis::TextVerticalAlignment::Top, mode );
566 }
567}
568
569QFontMetricsF QgsTextRenderer::fontMetrics( QgsRenderContext &context, const QgsTextFormat &format, const double scaleFactor )
570{
571 return QFontMetricsF( format.scaledFont( context, scaleFactor ), context.painter() ? context.painter()->device() : nullptr );
572}
573
574double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
575{
576 QPainter *p = context.painter();
577
578 Qgis::TextOrientation orientation = format.orientation();
580 {
581 constexpr double DEGREES_315_TO_RADIANS = 315 * ( M_PI / 180 );
582 constexpr double DEGREES_90_TO_RADIANS = 90 * ( M_PI / 180 );
583 constexpr double DEGREES_45_TO_RADIANS = 45 * ( M_PI / 180 );
584 if ( component.rotationRadians >= -DEGREES_315_TO_RADIANS && component.rotationRadians < -DEGREES_90_TO_RADIANS )
585 {
587 }
588 else if ( component.rotationRadians >= -DEGREES_90_TO_RADIANS && component.rotationRadians < -DEGREES_45_TO_RADIANS )
589 {
591 }
592 else
593 {
595 }
596 }
597
598 QgsTextBufferSettings buffer = format.buffer();
599
600 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
601 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
602
603 const double scaleFactor = calculateScaleFactorForFormat( context, format );
604
605 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
606 if ( mode == Qgis::TextLayoutMode::Labeling )
607 {
608 // label size has already been calculated using any symbology reference scale factor -- we need
609 // to temporarily remove the reference scale here or we'll be applying the scaling twice
610 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
611 }
612
613 if ( metrics.isNullFontSize() )
614 return 0;
615
616 referenceScaleOverride.reset();
617
618 QPainterPath path;
619 path.setFillRule( Qt::WindingFill );
620 double advance = 0;
621 double height = component.size.height();
622 switch ( orientation )
623 {
625 {
626 // NOT SUPPORTED BY THIS METHOD ANYMORE -- buffer drawing is handled in drawTextInternalHorizontal since QGIS 3.42
627 break;
628 }
629
632 {
633 double partYOffset = component.offset.y() * scaleFactor;
634
635 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( component.blockIndex );
636 double partLastDescent = 0;
637
638 int fragmentIndex = 0;
639 for ( const QgsTextFragment &fragment : component.block )
640 {
641 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, component.firstFragmentIndex + fragmentIndex );
642 const double letterSpacing = fragmentFont.letterSpacing() / scaleFactor;
643
644 const QFontMetricsF fragmentMetrics( fragmentFont );
645
646 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
647
648 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
649 for ( const QString &part : parts )
650 {
651 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
652 partYOffset += fragmentMetrics.ascent() / scaleFactor;
653 path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
654 partYOffset += letterSpacing;
655 }
656 partLastDescent = fragmentMetrics.descent() / scaleFactor;
657
658 fragmentIndex++;
659 }
660 height = partYOffset + partLastDescent;
661 advance = partYOffset - component.offset.y() * scaleFactor;
662 break;
663 }
664 }
665
666 QColor bufferColor = buffer.color();
667 bufferColor.setAlphaF( buffer.opacity() );
668 QPen pen( bufferColor );
669 pen.setWidthF( penSize * scaleFactor );
670 pen.setJoinStyle( buffer.joinStyle() );
671 QColor tmpColor( bufferColor );
672 // honor pref for whether to fill buffer interior
673 if ( !buffer.fillBufferInterior() )
674 {
675 tmpColor.setAlpha( 0 );
676 }
677
678 // store buffer's drawing in QPicture for drop shadow call
679 QPicture buffPict;
680 QPainter buffp;
681 buffp.begin( &buffPict );
682 if ( buffer.paintEffect() && buffer.paintEffect()->enabled() )
683 {
684 context.setPainter( &buffp );
685 std::unique_ptr< QgsPaintEffect > tmpEffect( buffer.paintEffect()->clone() );
686
687 tmpEffect->begin( context );
688 context.painter()->setPen( pen );
689 context.painter()->setBrush( tmpColor );
690 if ( scaleFactor != 1.0 )
691 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
692 context.painter()->drawPath( path );
693 if ( scaleFactor != 1.0 )
694 context.painter()->scale( scaleFactor, scaleFactor );
695 tmpEffect->end( context );
696
697 context.setPainter( p );
698 }
699 else
700 {
701 if ( scaleFactor != 1.0 )
702 buffp.scale( 1 / scaleFactor, 1 / scaleFactor );
703 buffp.setPen( pen );
704 buffp.setBrush( tmpColor );
705 buffp.drawPath( path );
706 }
707 buffp.end();
708
710 {
711 QgsTextRenderer::Component bufferComponent = component;
712 bufferComponent.origin = QPointF( 0.0, 0.0 );
713 bufferComponent.picture = buffPict;
714 bufferComponent.pictureBuffer = penSize / 2.0;
715 bufferComponent.size.setHeight( height );
716
718 {
719 bufferComponent.offset.setY( -bufferComponent.size.height() );
720 }
721 drawShadow( context, bufferComponent, format );
722 }
723
724 QgsScopedQPainterState painterState( p );
725 context.setPainterFlagsUsingContext( p );
726
728 {
729 p->setCompositionMode( buffer.blendMode() );
730 }
731
732 // scale for any print output or image saving @ specific dpi
733 p->scale( component.dpiRatio, component.dpiRatio );
735 p->drawPicture( 0, 0, buffPict );
736
737 return advance / scaleFactor;
738}
739
740void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
741{
742 QgsTextMaskSettings mask = format.mask();
743
744 // the mask is drawn to a side painter
745 // or to the main painter for preview
746 QPainter *p = context.isGuiPreview() ? context.painter() : context.maskPainter( context.currentMaskId() );
747 if ( !p )
748 return;
749
750 double penSize = mask.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * mask.size() / 100
751 : context.convertToPainterUnits( mask.size(), mask.sizeUnit(), mask.sizeMapUnitScale() );
752
753 // buffer: draw the text with a big pen
754 QPainterPath path;
755 path.setFillRule( Qt::WindingFill );
756
757 const double scaleFactor = calculateScaleFactorForFormat( context, format );
758
759 // TODO: vertical text mode was ignored when masking feature was added.
760 // Hopefully Oslandia come back and fix this? Hint hint...
761
762 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
763 if ( mode == Qgis::TextLayoutMode::Labeling )
764 {
765 // label size has already been calculated using any symbology reference scale factor -- we need
766 // to temporarily remove the reference scale here or we'll be applying the scaling twice
767 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
768 }
769
770 if ( metrics.isNullFontSize() )
771 return;
772
773 referenceScaleOverride.reset();
774
775 double xOffset = 0;
776 int fragmentIndex = 0;
777 for ( const QgsTextFragment &fragment : component.block )
778 {
779 if ( !fragment.isWhitespace() && !fragment.isImage() )
780 {
781 const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
782
783 const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
784 path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
785 }
786
787 xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ) * scaleFactor;
788 fragmentIndex++;
789 }
790
791 QColor bufferColor( Qt::gray );
792 bufferColor.setAlphaF( mask.opacity() );
793
794 QPen pen;
795 QBrush brush;
796 brush.setColor( bufferColor );
797 pen.setColor( bufferColor );
798 pen.setWidthF( penSize * scaleFactor );
799 pen.setJoinStyle( mask.joinStyle() );
800
801 QgsScopedQPainterState painterState( p );
802 context.setPainterFlagsUsingContext( p );
803
804 // scale for any print output or image saving @ specific dpi
805 p->scale( component.dpiRatio, component.dpiRatio );
806 if ( mask.paintEffect() && mask.paintEffect()->enabled() )
807 {
808 QgsPainterSwapper swapper( context, p );
809 {
810 QgsEffectPainter effectPainter( context, mask.paintEffect() );
811 if ( scaleFactor != 1.0 )
812 context.painter()->scale( 1 / scaleFactor, 1 / scaleFactor );
813 context.painter()->setPen( pen );
814 context.painter()->setBrush( brush );
815 context.painter()->drawPath( path );
816 if ( scaleFactor != 1.0 )
817 context.painter()->scale( scaleFactor, scaleFactor );
818 }
819 }
820 else
821 {
822 if ( scaleFactor != 1.0 )
823 p->scale( 1 / scaleFactor, 1 / scaleFactor );
824 p->setPen( pen );
825 p->setBrush( brush );
826 p->drawPath( path );
827 if ( scaleFactor != 1.0 )
828 p->scale( scaleFactor, scaleFactor );
829 }
830}
831
832double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF * )
833{
834 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( textLines, format );
835 if ( doc.size() == 0 )
836 return 0;
837
838 return textWidth( context, format, doc );
839}
840
841double QgsTextRenderer::textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
842{
843 //calculate max width of text lines
844 const double scaleFactor = calculateScaleFactorForFormat( context, format );
845
846 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
847
848 // width doesn't change depending on layout mode, we can use anything here
849 return metrics.documentSize( Qgis::TextLayoutMode::Point, format.orientation() ).width();
850}
851
853 const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode, QFontMetricsF *, Qgis::TextRendererFlags flags, double maxLineWidth
854)
855{
856 QStringList lines;
857 for ( const QString &line : textLines )
858 {
859 if ( flags & Qgis::TextRendererFlag::WrapLines && maxLineWidth > 0 && textRequiresWrapping( context, line, maxLineWidth, format ) )
860 {
861 lines.append( wrappedText( context, line, maxLineWidth, format ) );
862 }
863 else
864 {
865 lines.append( line );
866 }
867 }
868
869 const QgsTextDocument doc = QgsTextDocument::fromTextAndFormat( lines, format );
870 return textHeight( context, format, doc, mode );
871}
872
873double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, QChar character, bool includeEffects )
874{
875 const double scaleFactor = calculateScaleFactorForFormat( context, format );
876
877 bool isNullSize = false;
878 const QFont baseFont = format.scaledFont( context, scaleFactor, &isNullSize );
879 if ( isNullSize )
880 return 0;
881
882 const QFontMetrics fm( baseFont );
883 const double height = ( character.isNull() ? fm.height() : fm.boundingRect( character ).height() ) / scaleFactor;
884
885 if ( !includeEffects )
886 return height;
887
888 double maxExtension = 0;
889 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
890 if ( format.buffer().enabled() )
891 {
892 maxExtension += format.buffer().sizeUnit() == Qgis::RenderUnit::Percentage ? fontSize * format.buffer().size() / 100
893 : context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit(), format.buffer().sizeMapUnitScale() );
894 }
895 if ( format.shadow().enabled() )
896 {
897 maxExtension += ( format.shadow().offsetUnit() == Qgis::RenderUnit::Percentage
898 ? fontSize * format.shadow().offsetDistance() / 100
899 : context.convertToPainterUnits( format.shadow().offsetDistance(), format.shadow().offsetUnit(), format.shadow().offsetMapUnitScale() )
900 )
901 + ( format.shadow().blurRadiusUnit() == Qgis::RenderUnit::Percentage
902 ? fontSize * format.shadow().blurRadius() / 100
903 : context.convertToPainterUnits( format.shadow().blurRadius(), format.shadow().blurRadiusUnit(), format.shadow().blurRadiusMapUnitScale() )
904 );
905 }
906 if ( format.background().enabled() )
907 {
908 maxExtension += context.convertToPainterUnits( std::fabs( format.background().offset().y() ), format.background().offsetUnit(), format.background().offsetMapUnitScale() )
910 if ( format.background().sizeType() == QgsTextBackgroundSettings::SizeBuffer && format.background().size().height() > 0 )
911 {
912 maxExtension += context.convertToPainterUnits( format.background().size().height(), format.background().sizeUnit(), format.background().sizeMapUnitScale() );
913 }
914 }
915
916 return height + maxExtension;
917}
918
919bool QgsTextRenderer::textRequiresWrapping( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
920{
921 if ( qgsDoubleNear( width, 0.0 ) )
922 return false;
923
924 const QStringList multiLineSplit = text.split( '\n' );
925 const double currentTextWidth = QgsTextRenderer::textWidth( context, format, multiLineSplit );
926 return currentTextWidth > width;
927}
928
929QStringList QgsTextRenderer::wrappedText( const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format )
930{
931 const QStringList lines = text.split( '\n' );
932 QStringList outLines;
933 for ( const QString &line : lines )
934 {
935 if ( textRequiresWrapping( context, line, width, format ) )
936 {
937 //first step is to identify words which must be on their own line (too long to fit)
938 const QStringList words = line.split( ' ' );
939 QStringList linesToProcess;
940 QString wordsInCurrentLine;
941 for ( const QString &word : words )
942 {
943 if ( textRequiresWrapping( context, word, width, format ) )
944 {
945 //too long to fit
946 if ( !wordsInCurrentLine.isEmpty() )
947 linesToProcess << wordsInCurrentLine;
948 wordsInCurrentLine.clear();
949 linesToProcess << word;
950 }
951 else
952 {
953 if ( !wordsInCurrentLine.isEmpty() )
954 wordsInCurrentLine.append( ' ' );
955 wordsInCurrentLine.append( word );
956 }
957 }
958 if ( !wordsInCurrentLine.isEmpty() )
959 linesToProcess << wordsInCurrentLine;
960
961 for ( const QString &line : std::as_const( linesToProcess ) )
962 {
963 QString remainingText = line;
964 int lastPos = remainingText.lastIndexOf( ' ' );
965 while ( lastPos > -1 )
966 {
967 //check if remaining text is short enough to go in one line
968 if ( !textRequiresWrapping( context, remainingText, width, format ) )
969 {
970 break;
971 }
972
973 if ( !textRequiresWrapping( context, remainingText.left( lastPos ), width, format ) )
974 {
975 outLines << remainingText.left( lastPos );
976 remainingText = remainingText.mid( lastPos + 1 );
977 lastPos = 0;
978 }
979 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
980 }
981 outLines << remainingText;
982 }
983 }
984 else
985 {
986 outLines << line;
987 }
988 }
989
990 return outLines;
991}
992
993double QgsTextRenderer::textHeight( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &doc, Qgis::TextLayoutMode mode )
994{
995 QgsTextDocument document = doc;
996 document.applyCapitalization( format.capitalization() );
997
998 //calculate max height of text lines
999 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1000
1001 const QgsTextDocumentMetrics metrics = QgsTextDocumentMetrics::calculateMetrics( document, format, context, scaleFactor );
1002 if ( metrics.isNullFontSize() )
1003 return 0;
1004
1005 return metrics.documentSize( mode, format.orientation() ).height();
1006}
1007
1008void QgsTextRenderer::drawBackground( QgsRenderContext &context, const QgsTextRenderer::Component &c, const QgsTextFormat &format, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode )
1009{
1010 Component component = c;
1011 QgsTextBackgroundSettings background = format.background();
1012
1013 QPainter *prevP = context.painter();
1014 QPainter *p = context.painter();
1015 std::unique_ptr< QgsPaintEffect > tmpEffect;
1016 if ( background.paintEffect() && background.paintEffect()->enabled() )
1017 {
1018 tmpEffect.reset( background.paintEffect()->clone() );
1019 tmpEffect->begin( context );
1020 p = context.painter();
1021 }
1022
1023 //QgsDebugMsgLevel( u"Background label rotation: %1"_s.arg( component.rotation() ), 4 );
1024
1025 // shared calculations between shapes and SVG
1026
1027 // configure angles, set component rotation and rotationOffset
1028 const double originAdjustRotationRadians = -component.rotationRadians;
1030 {
1031 component.rotationRadians = -( component.rotationRadians ); // RotationSync
1032 component.rotationOffsetRadians = background.rotationType() == QgsTextBackgroundSettings::RotationOffset ? ( background.rotation() * M_PI / 180 ) : 0.0;
1033 }
1034 else // RotationFixed
1035 {
1036 component.rotationRadians = 0.0; // don't use label's rotation
1037 component.rotationOffsetRadians = background.rotation() * M_PI / 180;
1038 }
1039
1040 const double scaleFactor = calculateScaleFactorForFormat( context, format );
1041
1042 if ( mode != Qgis::TextLayoutMode::Labeling )
1043 {
1044 // need to calculate size of text
1045 const QSizeF documentSize = metrics.documentSize( mode, format.orientation() );
1046 double width = documentSize.width();
1047 double height = documentSize.height();
1048
1049 switch ( mode )
1050 {
1054 switch ( component.hAlign )
1055 {
1058 component.center = QPointF( component.origin.x() + width / 2.0, component.origin.y() + height / 2.0 );
1059 break;
1060
1062 component.center = QPointF( component.origin.x() + component.size.width() / 2.0, component.origin.y() + height / 2.0 );
1063 break;
1064
1066 component.center = QPointF( component.origin.x() + component.size.width() - width / 2.0, component.origin.y() + height / 2.0 );
1067 break;
1068 }
1069 break;
1070
1072 {
1073 bool isNullSize = false;
1074 QFontMetricsF fm( format.scaledFont( context, scaleFactor, &isNullSize ) );
1075 double originAdjust = isNullSize ? 0 : ( fm.ascent() / scaleFactor / 2.0 - fm.leading() / scaleFactor / 2.0 );
1076 switch ( component.hAlign )
1077 {
1080 component.center = QPointF( component.origin.x() + width / 2.0, component.origin.y() - height / 2.0 + originAdjust );
1081 break;
1082
1084 component.center = QPointF( component.origin.x(), component.origin.y() - height / 2.0 + originAdjust );
1085 break;
1086
1088 component.center = QPointF( component.origin.x() - width / 2.0, component.origin.y() - height / 2.0 + originAdjust );
1089 break;
1090 }
1091
1092 // apply rotation to center point
1093 if ( !qgsDoubleNear( originAdjustRotationRadians, 0 ) )
1094 {
1095 const double dx = component.center.x() - component.origin.x();
1096 const double dy = component.center.y() - component.origin.y();
1097 component.center.setX( component.origin.x() + ( std::cos( originAdjustRotationRadians ) * dx - std::sin( originAdjustRotationRadians ) * dy ) );
1098 component.center.setY( component.origin.y() + ( std::sin( originAdjustRotationRadians ) * dx + std::cos( originAdjustRotationRadians ) * dy ) );
1099 }
1100 break;
1101 }
1102
1104 break;
1105 }
1106
1108 component.size = QSizeF( width, height );
1109 }
1110
1111 // TODO: the following label-buffered generated shapes and SVG symbols should be moved into marker symbology classes
1112
1113 switch ( background.type() )
1114 {
1117 {
1118 // all calculations done in shapeSizeUnits, which are then passed to symbology class for painting
1119
1120 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG && background.svgFile().isEmpty() )
1121 return;
1122
1123 if ( background.type() == QgsTextBackgroundSettings::ShapeMarkerSymbol && !background.markerSymbol() )
1124 return;
1125
1126 double sizeOut = 0.0;
1127 {
1128 QgsScopedRenderContextReferenceScaleOverride referenceScaleOverride( context, -1 );
1129
1130 // only one size used for SVG/marker symbol sizing/scaling (no use of shapeSize.y() or Y field in gui)
1131 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1132 {
1133 sizeOut = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1134 }
1135 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1136 {
1137 sizeOut = std::max( component.size.width(), component.size.height() );
1138 double bufferSize = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1139
1140 // add buffer
1141 sizeOut += bufferSize * 2;
1142 }
1143 }
1144
1145 // don't bother rendering symbols smaller than 1x1 pixels in size
1146 // TODO: add option to not show any svgs under/over a certain size
1147 if ( sizeOut < 1.0 )
1148 return;
1149
1150 std::unique_ptr< QgsMarkerSymbol > renderedSymbol;
1151 if ( background.type() == QgsTextBackgroundSettings::ShapeSVG )
1152 {
1153 QVariantMap map; // for SVG symbology marker
1154 map[u"name"_s] = background.svgFile().trimmed();
1155 map[u"size"_s] = QString::number( sizeOut );
1156 map[u"size_unit"_s] = QgsUnitTypes::encodeUnit( Qgis::RenderUnit::Pixels );
1157 map[u"angle"_s] = QString::number( 0.0 ); // angle is handled by this local painter
1158
1159 // offset is handled by this local painter
1160 // TODO: see why the marker renderer doesn't seem to translate offset *after* applying rotation
1161 //map["offset"] = QgsSymbolLayerUtils::encodePoint( tmpLyr.shapeOffset );
1162 //map["offset_unit"] = QgsUnitTypes::encodeUnit(
1163 // tmpLyr.shapeOffsetUnits == QgsPalLayerSettings::MapUnits ? QgsUnitTypes::MapUnit : QgsUnitTypes::MM );
1164
1165 map[u"fill"_s] = background.fillColor().name();
1166 map[u"outline"_s] = background.strokeColor().name();
1167 map[u"outline-width"_s] = QString::number( background.strokeWidth() );
1168 map[u"outline_width_unit"_s] = QgsUnitTypes::encodeUnit( background.strokeWidthUnit() );
1169
1171 {
1172 QgsTextShadowSettings shadow = format.shadow();
1173 // configure SVG shadow specs
1174 QVariantMap shdwmap( map );
1175 shdwmap[u"fill"_s] = shadow.color().name();
1176 shdwmap[u"outline"_s] = shadow.color().name();
1177 shdwmap[u"size"_s] = QString::number( sizeOut );
1178
1179 // store SVG's drawing in QPicture for drop shadow call
1180 QPicture svgPict;
1181 QPainter svgp;
1182 svgp.begin( &svgPict );
1183
1184 // draw shadow symbol
1185
1186 // clone current render context map unit/mm conversion factors, but not
1187 // other map canvas parameters, then substitute this painter for use in symbology painting
1188 // NOTE: this is because the shadow needs to be scaled correctly for output to map canvas,
1189 // but will be created relative to the SVG's computed size, not the current map canvas
1190 QgsRenderContext shdwContext;
1191 shdwContext.setMapToPixel( context.mapToPixel() );
1192 shdwContext.setScaleFactor( context.scaleFactor() );
1193 shdwContext.setPainter( &svgp );
1194
1195 std::unique_ptr< QgsSymbolLayer > symShdwL( QgsSvgMarkerSymbolLayer::create( shdwmap ) );
1196 QgsSvgMarkerSymbolLayer *svgShdwM = static_cast<QgsSvgMarkerSymbolLayer *>( symShdwL.get() );
1197 QgsSymbolRenderContext svgShdwContext( shdwContext, Qgis::RenderUnit::Unknown, background.opacity() );
1198
1199 svgShdwM->renderPoint( QPointF( sizeOut / 2, -sizeOut / 2 ), svgShdwContext );
1200 svgp.end();
1201
1202 component.picture = svgPict;
1203 // TODO: when SVG symbol's stroke width/units is fixed in QgsSvgCache, adjust for it here
1204 component.pictureBuffer = 0.0;
1205
1206 component.size = QSizeF( sizeOut, sizeOut );
1207 component.offset = QPointF( 0.0, 0.0 );
1208
1209 // rotate about origin center of SVG
1210 QgsScopedQPainterState painterState( p );
1211 context.setPainterFlagsUsingContext( p );
1212
1213 p->translate( component.center.x(), component.center.y() );
1214 p->rotate( component.rotationRadians * 180 / M_PI );
1215 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1216 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1217 p->translate( QPointF( xoff, yoff ) );
1218 p->rotate( component.rotationOffsetRadians * 180 / M_PI );
1219 p->translate( -sizeOut / 2, sizeOut / 2 );
1220
1221 drawShadow( context, component, format );
1222 }
1223 renderedSymbol.reset();
1224
1225 std::unique_ptr<QgsSymbolLayer> symL = QgsSvgMarkerSymbolLayer::create( map );
1226 renderedSymbol = std::make_unique<QgsMarkerSymbol>( QgsSymbolLayerList() << symL.release() );
1227 }
1228 else
1229 {
1230 renderedSymbol.reset( background.markerSymbol()->clone() );
1231 renderedSymbol->setSize( sizeOut );
1232 renderedSymbol->setSizeUnit( Qgis::RenderUnit::Pixels );
1233 }
1234
1235 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1236
1237 // draw the actual symbol
1238 QgsScopedQPainterState painterState( p );
1239 context.setPainterFlagsUsingContext( p );
1240
1242 {
1243 p->setCompositionMode( background.blendMode() );
1244 }
1245 p->translate( component.center.x(), component.center.y() );
1246 p->rotate( component.rotationRadians * 180 / M_PI );
1247 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1248 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1249 p->translate( QPointF( xoff, yoff ) );
1250 p->rotate( component.rotationOffsetRadians * 180 / M_PI );
1251
1252 const QgsFeature f = context.expressionContext().feature();
1253 renderedSymbol->startRender( context, context.expressionContext().fields() );
1254 renderedSymbol->renderPoint( QPointF( 0, 0 ), &f, context );
1255 renderedSymbol->stopRender( context );
1256 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1257
1258 break;
1259 }
1260
1265 {
1266 double w = component.size.width();
1267 double h = component.size.height();
1268
1269 if ( background.sizeType() == QgsTextBackgroundSettings::SizeFixed )
1270 {
1271 w = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1272 h = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), background.sizeMapUnitScale() );
1273 }
1274 else if ( background.sizeType() == QgsTextBackgroundSettings::SizeBuffer )
1275 {
1276 if ( background.type() == QgsTextBackgroundSettings::ShapeSquare )
1277 {
1278 if ( w > h )
1279 h = w;
1280 else if ( h > w )
1281 w = h;
1282 }
1283 else if ( background.type() == QgsTextBackgroundSettings::ShapeCircle )
1284 {
1285 // start with label bound by circle
1286 h = std::sqrt( std::pow( w, 2 ) + std::pow( h, 2 ) );
1287 w = h;
1288 }
1289 else if ( background.type() == QgsTextBackgroundSettings::ShapeEllipse )
1290 {
1291 // start with label bound by ellipse
1292 h = h * M_SQRT1_2 * 2;
1293 w = w * M_SQRT1_2 * 2;
1294 }
1295
1296 double bufferWidth = context.convertToPainterUnits( background.size().width(), background.sizeUnit(), background.sizeMapUnitScale() );
1297 double bufferHeight = context.convertToPainterUnits( background.size().height(), background.sizeUnit(), background.sizeMapUnitScale() );
1298
1299 w += bufferWidth * 2;
1300 h += bufferHeight * 2;
1301 }
1302
1303 // offsets match those of symbology: -x = left, -y = up
1304 QRectF rect( -w / 2.0, -h / 2.0, w, h );
1305
1306 if ( rect.isNull() )
1307 return;
1308
1309 QgsScopedQPainterState painterState( p );
1310 context.setPainterFlagsUsingContext( p );
1311
1312 p->translate( QPointF( component.center.x(), component.center.y() ) );
1313 p->rotate( component.rotationRadians * 180 / M_PI );
1314 double xoff = context.convertToPainterUnits( background.offset().x(), background.offsetUnit(), background.offsetMapUnitScale() );
1315 double yoff = context.convertToPainterUnits( background.offset().y(), background.offsetUnit(), background.offsetMapUnitScale() );
1316 p->translate( QPointF( xoff, yoff ) );
1317 p->rotate( component.rotationOffsetRadians * 180 / M_PI );
1318
1319 QPainterPath path;
1320
1321 // Paths with curves must be enlarged before conversion to QPolygonF, or
1322 // the curves are approximated too much and appear jaggy
1323 QTransform t = QTransform::fromScale( 10, 10 );
1324 // inverse transform used to scale created polygons back to expected size
1325 QTransform ti = t.inverted();
1326
1328 {
1329 if ( background.radiiUnit() == Qgis::RenderUnit::Percentage )
1330 {
1331 path.addRoundedRect( rect, background.radii().width(), background.radii().height(), Qt::RelativeSize );
1332 }
1333 else
1334 {
1335 const double xRadius = context.convertToPainterUnits( background.radii().width(), background.radiiUnit(), background.radiiMapUnitScale() );
1336 const double yRadius = context.convertToPainterUnits( background.radii().height(), background.radiiUnit(), background.radiiMapUnitScale() );
1337 path.addRoundedRect( rect, xRadius, yRadius );
1338 }
1339 }
1341 {
1342 path.addEllipse( rect );
1343 }
1344 QPolygonF tempPolygon = path.toFillPolygon( t );
1345 QPolygonF polygon = ti.map( tempPolygon );
1346 QPicture shapePict;
1347 QPainter *oldp = context.painter();
1348 QPainter shapep;
1349
1350 shapep.begin( &shapePict );
1351 context.setPainter( &shapep );
1352
1353 std::unique_ptr< QgsFillSymbol > renderedSymbol;
1354 renderedSymbol.reset( background.fillSymbol()->clone() );
1355 renderedSymbol->setOpacity( renderedSymbol->opacity() * background.opacity() );
1356
1357 const QgsFeature f = context.expressionContext().feature();
1358 renderedSymbol->startRender( context, context.expressionContext().fields() );
1359 renderedSymbol->renderPolygon( polygon, nullptr, &f, context );
1360 renderedSymbol->stopRender( context );
1361
1362 shapep.end();
1363 context.setPainter( oldp );
1364
1366 {
1367 component.picture = shapePict;
1368 component.pictureBuffer = QgsSymbolLayerUtils::estimateMaxSymbolBleed( renderedSymbol.get(), context ) * 2;
1369
1370 component.size = rect.size();
1371 component.offset = QPointF( rect.width() / 2, -rect.height() / 2 );
1372 drawShadow( context, component, format );
1373 }
1374
1376 {
1377 p->setCompositionMode( background.blendMode() );
1378 }
1379
1380 // scale for any print output or image saving @ specific dpi
1381 p->scale( component.dpiRatio, component.dpiRatio );
1383 p->drawPicture( 0, 0, shapePict );
1384 p->setCompositionMode( QPainter::CompositionMode_SourceOver ); // just to be sure
1385 break;
1386 }
1387 }
1388
1389 if ( tmpEffect )
1390 {
1391 tmpEffect->end( context );
1392 context.setPainter( prevP );
1393 }
1394}
1395
1396void QgsTextRenderer::drawShadow( QgsRenderContext &context, const QgsTextRenderer::Component &component, const QgsTextFormat &format )
1397{
1398 QgsTextShadowSettings shadow = format.shadow();
1399
1400 QPainter *p = context.painter();
1401 const double componentWidth = component.size.width();
1402 const double componentHeight = component.size.height();
1403 const double xOffset = component.offset.x();
1404 const double yOffset = component.offset.y();
1405 double pictbuffer = component.pictureBuffer;
1406
1407 // generate pixmap representation of label component drawing
1408 bool mapUnits = shadow.blurRadiusUnit() == Qgis::RenderUnit::MapUnits;
1409
1410 const double fontSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
1411 double radius = shadow.blurRadiusUnit() == Qgis::RenderUnit::Percentage ? fontSize * shadow.blurRadius() / 100
1412 : context.convertToPainterUnits( shadow.blurRadius(), shadow.blurRadiusUnit(), shadow.blurRadiusMapUnitScale() );
1413 radius /= ( mapUnits ? context.scaleFactor() / component.dpiRatio : 1 );
1414 radius = static_cast< int >( radius + 0.5 ); //NOLINT
1415
1416 // TODO: add labeling gui option to adjust blurBufferClippingScale to minimize pixels, or
1417 // to ensure shadow isn't clipped too tight. (Or, find a better method of buffering)
1418 double blurBufferClippingScale = 3.75;
1419 int blurbuffer = ( radius > 17 ? 16 : radius ) * blurBufferClippingScale;
1420
1421 QImage blurImg( componentWidth + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ), componentHeight + ( pictbuffer * 2.0 ) + ( blurbuffer * 2.0 ), QImage::Format_ARGB32_Premultiplied );
1422
1423 // TODO: add labeling gui option to not show any shadows under/over a certain size
1424 // keep very small QImages from causing paint device issues, i.e. must be at least > 1
1425 int minBlurImgSize = 1;
1426 // max limitation on QgsSvgCache is 10,000 for screen, which will probably be reasonable for future caching here, too
1427 // 4 x QgsSvgCache limit for output to print/image at higher dpi
1428 // TODO: should it be higher, scale with dpi, or have no limit? Needs testing with very large labels rendered at high dpi output
1429 int maxBlurImgSize = 40000;
1430 if ( blurImg.isNull() || ( blurImg.width() < minBlurImgSize || blurImg.height() < minBlurImgSize ) || ( blurImg.width() > maxBlurImgSize || blurImg.height() > maxBlurImgSize ) )
1431 return;
1432
1433 blurImg.fill( QColor( Qt::transparent ).rgba() );
1434 QPainter pictp;
1435 if ( !pictp.begin( &blurImg ) )
1436 return;
1437 pictp.setRenderHints( QPainter::Antialiasing | QPainter::SmoothPixmapTransform );
1438 QPointF imgOffset( blurbuffer + pictbuffer + xOffset, blurbuffer + pictbuffer + componentHeight + yOffset );
1439
1440 pictp.drawPicture( imgOffset, component.picture );
1441
1442 // overlay shadow color
1443 pictp.setCompositionMode( QPainter::CompositionMode_SourceIn );
1444 pictp.fillRect( blurImg.rect(), shadow.color() );
1445 pictp.end();
1446
1447 // blur the QImage in-place
1448 if ( shadow.blurRadius() > 0.0 && radius > 0 )
1449 {
1450 QgsSymbolLayerUtils::blurImageInPlace( blurImg, blurImg.rect(), radius, shadow.blurAlphaOnly() );
1451 }
1452
1453#if 0
1454 // debug rect for QImage shadow registration and clipping visualization
1455 QPainter picti;
1456 picti.begin( &blurImg );
1457 picti.setBrush( Qt::Dense7Pattern );
1458 QPen imgPen( QColor( 0, 0, 255, 255 ) );
1459 imgPen.setWidth( 1 );
1460 picti.setPen( imgPen );
1461 picti.setOpacity( 0.1 );
1462 picti.drawRect( 0, 0, blurImg.width(), blurImg.height() );
1463 picti.end();
1464#endif
1465
1466 const double offsetDist = shadow.offsetUnit() == Qgis::RenderUnit::Percentage ? fontSize * shadow.offsetDistance() / 100
1467 : context.convertToPainterUnits( shadow.offsetDistance(), shadow.offsetUnit(), shadow.offsetMapUnitScale() );
1468 double angleRad = shadow.offsetAngle() * M_PI / 180; // to radians
1469 if ( shadow.offsetGlobal() )
1470 {
1471 // TODO: check for differences in rotation origin and cw/ccw direction,
1472 // when this shadow function is used for something other than labels
1473
1474 // it's 0-->cw-->360 for labels
1475 //QgsDebugMsgLevel( u"Shadow aggregated label rotation (radians): %1"_s.arg( component.rotation() + component.rotationOffset() ), 4 );
1476 angleRad -= ( component.rotationRadians + component.rotationOffsetRadians );
1477 }
1478
1479 QPointF transPt( -offsetDist * std::cos( angleRad + M_PI_2 ), -offsetDist * std::sin( angleRad + M_PI_2 ) );
1480
1481 QgsScopedQPainterState painterState( p );
1482 context.setPainterFlagsUsingContext( p );
1483 // this was historically ALWAYS set for text renderer. We may want to consider getting it to respect the
1484 // corresponding flag in the render context instead...
1485 p->setRenderHint( QPainter::SmoothPixmapTransform );
1487 {
1488 p->setCompositionMode( shadow.blendMode() );
1489 }
1490 p->setOpacity( shadow.opacity() );
1491
1492 double scale = shadow.scale() / 100.0;
1493 // TODO: scale from center/center, left/center or left/top, instead of default left/bottom?
1494 p->scale( scale, scale );
1495 if ( component.useOrigin )
1496 {
1497 p->translate( component.origin.x(), component.origin.y() );
1498 }
1499 p->translate( transPt );
1500 p->translate( -imgOffset.x(), -imgOffset.y() );
1501 p->drawImage( 0, 0, blurImg );
1502
1503 painterState.restore();
1504
1505 // debug rects
1506#if 0
1507 // draw debug rect for QImage painting registration
1508 p->save();
1509 p->setBrush( Qt::NoBrush );
1510 QPen imgPen( QColor( 255, 0, 0, 10 ) );
1511 imgPen.setWidth( 2 );
1512 imgPen.setStyle( Qt::DashLine );
1513 p->setPen( imgPen );
1514 p->scale( scale, scale );
1515 if ( component.useOrigin() )
1516 {
1517 p->translate( component.origin().x(), component.origin().y() );
1518 }
1519 p->translate( transPt );
1520 p->translate( -imgOffset.x(),
1521 -imgOffset.y() );
1522 p->drawRect( 0, 0, blurImg.width(), blurImg.height() );
1523 p->restore();
1524
1525 // draw debug rect for passed in component dimensions
1526 p->save();
1527 p->setBrush( Qt::NoBrush );
1528 QPen componentRectPen( QColor( 0, 255, 0, 70 ) );
1529 componentRectPen.setWidth( 1 );
1530 if ( component.useOrigin() )
1531 {
1532 p->translate( component.origin().x(), component.origin().y() );
1533 }
1534 p->setPen( componentRectPen );
1535 p->drawRect( QRect( -xOffset, -componentHeight - yOffset, componentWidth, componentHeight ) );
1536 p->restore();
1537#endif
1538}
1539
1540
1541void QgsTextRenderer::drawTextInternal(
1542 Qgis::TextComponents components,
1543 QgsRenderContext &context,
1544 const QgsTextFormat &format,
1545 const Component &component,
1546 const QgsTextDocument &document,
1547 const QgsTextDocumentMetrics &metrics,
1549 Qgis::TextVerticalAlignment vAlignment,
1551)
1552{
1553 if ( !context.painter() )
1554 {
1555 return;
1556 }
1557
1558 const double fontScale = calculateScaleFactorForFormat( context, format );
1559
1560 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
1561 if ( mode == Qgis::TextLayoutMode::Labeling )
1562 {
1563 // label size has already been calculated using any symbology reference scale factor -- we need
1564 // to temporarily remove the reference scale here or we'll be applying the scaling twice
1565 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
1566 }
1567
1568 if ( metrics.isNullFontSize() )
1569 return;
1570
1571 referenceScaleOverride.reset();
1572
1573 double rotationDegrees = 0;
1574 const Qgis::TextOrientation orientation = calculateRotationAndOrientationForComponent( format, component, rotationDegrees );
1575 switch ( orientation )
1576 {
1578 {
1579 drawTextInternalHorizontal( context, format, components, mode, component, document, metrics, fontScale, alignment, vAlignment, rotationDegrees );
1580 break;
1581 }
1582
1585 {
1586 // TODO: vertical text renderer currently doesn't handle one-pass buffer + text drawing
1587 if ( components & Qgis::TextComponent::Buffer )
1588 drawTextInternalVertical( context, format, Qgis::TextComponent::Buffer, mode, component, document, metrics, fontScale, alignment, vAlignment, rotationDegrees );
1589 if ( components & Qgis::TextComponent::Text )
1590 drawTextInternalVertical( context, format, Qgis::TextComponent::Text, mode, component, document, metrics, fontScale, alignment, vAlignment, rotationDegrees );
1591 break;
1592 }
1593 }
1594}
1595
1596Qgis::TextOrientation QgsTextRenderer::calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const QgsTextRenderer::Component &component, double &rotationDegrees )
1597{
1598 rotationDegrees = -component.rotationRadians * 180 / M_PI;
1599
1600 switch ( format.orientation() )
1601 {
1603 {
1604 // Between 45 to 135 and 235 to 315 degrees, rely on vertical orientation
1605 if ( rotationDegrees >= -315 && rotationDegrees < -90 )
1606 {
1607 rotationDegrees -= 90;
1609 }
1610 else if ( rotationDegrees >= -90 && rotationDegrees < -45 )
1611 {
1612 rotationDegrees += 90;
1614 }
1615
1617 }
1618
1621 return format.orientation();
1622 }
1624}
1625
1626void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
1627{
1628 const QString blockText = block.toPlainText();
1629 QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
1630 finder.toStart();
1631 int wordBoundaries = 0;
1632 while ( finder.toNextBoundary() != -1 )
1633 {
1634 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1635 wordBoundaries++;
1636 }
1637
1638 if ( wordBoundaries > 0 )
1639 {
1640 // word boundaries found => justify by padding word spacing
1641 extraWordSpace = spaceToDistribute / wordBoundaries;
1642 }
1643 else
1644 {
1645 // no word boundaries found => justify by letter spacing
1646 QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
1647 finder.toStart();
1648
1649 int graphemeBoundaries = 0;
1650 while ( finder.toNextBoundary() != -1 )
1651 {
1652 if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
1653 graphemeBoundaries++;
1654 }
1655
1656 if ( graphemeBoundaries > 0 )
1657 {
1658 extraLetterSpace = spaceToDistribute / graphemeBoundaries;
1659 }
1660 }
1661}
1662
1663void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
1664{
1665 const double prevWordSpace = font.wordSpacing();
1666 font.setWordSpacing( prevWordSpace + extraWordSpace );
1667 const double prevLetterSpace = font.letterSpacing();
1668 font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
1669}
1670
1671
1672void QgsTextRenderer::renderBlockHorizontal(
1673 const QgsTextBlock &block,
1674 int blockIndex,
1675 const QgsTextDocumentMetrics &metrics,
1676 QgsRenderContext &context,
1677 const QgsTextFormat &format,
1678 QPainter *painter,
1679 bool forceRenderAsPaths,
1680 double fontScale,
1681 double extraWordSpace,
1682 double extraLetterSpace,
1684 DeferredRenderBlock *deferredRenderBlock
1685)
1686{
1687 if ( !metrics.isNullFontSize() )
1688 {
1689 double xOffset = 0;
1690 int fragmentIndex = 0;
1691 for ( const QgsTextFragment &fragment : block )
1692 {
1693 // draw text, QPainterPath method
1694 if ( !fragment.isWhitespace() && !fragment.isImage() )
1695 {
1696 QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
1697
1698 if ( !qgsDoubleNear( extraWordSpace, 0 ) || !qgsDoubleNear( extraLetterSpace, 0 ) )
1699 applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
1700
1701 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1702
1703 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
1704 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
1705
1706 if ( deferredRenderBlock )
1707 {
1708 DeferredRenderFragment renderFragment;
1709 renderFragment.color = textColor;
1710 if ( forceRenderAsPaths )
1711 {
1712 renderFragment.path.setFillRule( Qt::WindingFill );
1713 renderFragment.path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1714 }
1715 renderFragment.font = fragmentFont;
1716 renderFragment.point = QPointF( xOffset, yOffset );
1717 renderFragment.text = fragment.text();
1718 deferredRenderBlock->fragments.append( renderFragment );
1719 }
1720 else if ( forceRenderAsPaths )
1721 {
1722 painter->setBrush( textColor );
1723 QPainterPath path;
1724 path.setFillRule( Qt::WindingFill );
1725 path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
1726 painter->drawPath( path );
1727 }
1728 else
1729 {
1730 painter->setPen( textColor );
1731 painter->setFont( fragmentFont );
1732 painter->drawText( QPointF( xOffset, yOffset ), fragment.text() );
1733 }
1734 }
1735 else if ( fragment.isImage() )
1736 {
1737 bool fitsInCache = false;
1738 const double imageWidth = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1739 const double imageHeight = metrics.fragmentFixedHeight( blockIndex, fragmentIndex, mode ) * fontScale;
1740
1741 const QImage image = QgsApplication::imageCache()->pathAsImage(
1742 fragment.characterFormat().imagePath(),
1743 QSize( static_cast< int >( std::round( imageWidth ) ), static_cast< int >( std::round( imageHeight ) ) ),
1744 false,
1745 1,
1746 fitsInCache,
1748 );
1749 const double imageBaseline = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1750 const double yOffset = imageBaseline - image.height();
1751 if ( !image.isNull() )
1752 painter->drawImage( QPointF( xOffset, yOffset ), image );
1753 }
1754
1755 xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ) * fontScale;
1756 fragmentIndex++;
1757 }
1758 }
1759};
1760
1761bool QgsTextRenderer::usePathsToRender( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document )
1762{
1763 switch ( context.textRenderFormat() )
1764 {
1766 return true;
1768 return false;
1770 {
1771 // Prefer not to use paths -- but certain conditions will require us to use them
1772 if ( format.buffer().enabled() )
1773 {
1774 // text buffer requires use of paths
1775 // TODO: this was the original cause of use switching from text to paths by default,
1776 // but that was way back in the 2.0 days and maybe the Qt issues have now been fixed?
1777 return true;
1778 }
1779
1780 // underline/overline/strikethrough looks different between path/non-path renders.
1781 // TODO: validate which is correct. For now, maintain default appearance from before this code
1782 // was introduced
1783 if ( format.font().underline() || format.font().overline() || format.font().strikeOut() || std::any_of( document.begin(), document.end(), []( const QgsTextBlock &block ) {
1784 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment &fragment ) {
1785 return fragment.characterFormat().underline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1786 || fragment.characterFormat().overline() == QgsTextCharacterFormat::BooleanValue::SetTrue
1787 || fragment.characterFormat().strikeOut() == QgsTextCharacterFormat::BooleanValue::SetTrue;
1788 } );
1789 } ) )
1790 return true;
1791
1792 return false;
1793 }
1794 }
1796}
1797
1798bool QgsTextRenderer::usePictureToRender( const QgsRenderContext &, const QgsTextFormat &, const QgsTextDocument &document )
1799{
1800 return std::any_of( document.begin(), document.end(), []( const QgsTextBlock &block ) {
1801 return std::any_of( block.begin(), block.end(), []( const QgsTextFragment &fragment ) { return fragment.isImage(); } );
1802 } );
1803}
1804
1805QVector< QgsTextRenderer::BlockMetrics > QgsTextRenderer::calculateBlockMetrics(
1806 const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, Qgis::TextLayoutMode mode, double targetWidth, const Qgis::TextHorizontalAlignment hAlignment
1807)
1808{
1809 QVector< BlockMetrics > blockMetrics;
1810 blockMetrics.reserve( document.size() );
1811
1812 int blockIndex = 0;
1813 for ( const QgsTextBlock &block : document )
1814 {
1815 Qgis::TextHorizontalAlignment blockAlignment = hAlignment;
1816 if ( block.blockFormat().hasHorizontalAlignmentSet() )
1817 blockAlignment = block.blockFormat().horizontalAlignment();
1818 const bool adjustForAlignment = blockAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || document.size() > 1 );
1819
1820 const bool isFinalLineInParagraph = ( blockIndex == document.size() - 1 ) || document.at( blockIndex + 1 ).toPlainText().trimmed().isEmpty();
1821
1822 BlockMetrics thisBlockMetrics;
1823 // figure x offset for horizontal alignment of multiple lines
1824 thisBlockMetrics.width = metrics.blockWidth( blockIndex );
1825
1826 if ( adjustForAlignment )
1827 {
1828 double blockWidthDiff = 0;
1829 switch ( blockAlignment )
1830 {
1832 blockWidthDiff = ( targetWidth - thisBlockMetrics.width - metrics.blockLeftMargin( blockIndex ) - metrics.blockRightMargin( blockIndex ) ) * 0.5 + metrics.blockLeftMargin( blockIndex );
1833 break;
1834
1836 blockWidthDiff = targetWidth - thisBlockMetrics.width - metrics.blockRightMargin( blockIndex );
1837 break;
1838
1840 if ( !isFinalLineInParagraph && targetWidth > thisBlockMetrics.width )
1841 {
1842 calculateExtraSpacingForLineJustification( targetWidth - thisBlockMetrics.width, block, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace );
1843 thisBlockMetrics.width = targetWidth;
1844 }
1845 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1846 break;
1847
1849 blockWidthDiff = metrics.blockLeftMargin( blockIndex );
1850 break;
1851 }
1852
1853 switch ( mode )
1854 {
1859 thisBlockMetrics.xOffset = blockWidthDiff;
1860 break;
1861
1863 {
1864 switch ( blockAlignment )
1865 {
1867 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth;
1868 break;
1869
1871 thisBlockMetrics.xOffset = blockWidthDiff - targetWidth / 2.0;
1872 break;
1873
1876 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1877 break;
1878 }
1879 }
1880 break;
1881 }
1882 }
1883 else if ( blockAlignment == Qgis::TextHorizontalAlignment::Left || blockAlignment == Qgis::TextHorizontalAlignment::Justify )
1884 {
1885 thisBlockMetrics.xOffset = metrics.blockLeftMargin( blockIndex );
1886 }
1887
1888 switch ( mode )
1889 {
1893 thisBlockMetrics.backgroundWidth = targetWidth;
1894 thisBlockMetrics.backgroundXOffset = 0;
1895 break;
1898 thisBlockMetrics.backgroundWidth = thisBlockMetrics.width;
1899 thisBlockMetrics.backgroundXOffset = thisBlockMetrics.xOffset;
1900 break;
1901 }
1902
1903 blockMetrics << thisBlockMetrics;
1904 blockIndex++;
1905 }
1906 return blockMetrics;
1907}
1908
1909QBrush QgsTextRenderer::createBrushForPath( QgsRenderContext &context, const QString &path )
1910{
1911 bool fitsInCache = false;
1912 // use original image size
1913 const QSize imageSize = QgsApplication::imageCache()->originalSize( path, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1914 // TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
1915 const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
1916 const double pixelsPerMm = context.scaleFactor();
1917 const double imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
1918 const double imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
1919 QBrush res;
1920 if ( imageWidth == 0 || imageHeight == 0 )
1921 return res;
1922 const QImage image
1924 ->pathAsImage( path, QSize( static_cast< int >( std::round( imageWidth ) ), static_cast< int >( std::round( imageHeight ) ) ), false, 1, fitsInCache, context.flags() & Qgis::RenderContextFlag::RenderBlocking );
1925
1926 if ( !image.isNull() )
1927 {
1928 res.setTextureImage( image );
1929 }
1930 return res;
1931}
1932
1933void QgsTextRenderer::renderDocumentBackgrounds(
1934 QgsRenderContext &context,
1935 const QgsTextDocument &document,
1936 const QgsTextDocumentMetrics &metrics,
1937 const Component &component,
1938 const QVector< QgsTextRenderer::BlockMetrics > &blockMetrics,
1940 double verticalAlignOffset,
1941 double rotationDegrees
1942)
1943{
1944 int blockIndex = 0;
1945 context.painter()->translate( component.origin );
1946 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
1947 context.painter()->rotate( rotationDegrees );
1948
1949 context.painter()->setPen( Qt::NoPen );
1950 context.painter()->setBrush( Qt::NoBrush );
1951 for ( const QgsTextBlock &block : document )
1952 {
1953 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
1954 const double blockMaximumDescent = metrics.blockMaximumDescent( blockIndex );
1955 const double blockMaximumAscent = metrics.blockMaximumAscent( blockIndex );
1956
1957 if ( block.blockFormat().hasBackground() )
1958 {
1959 QBrush backgroundBrush = block.blockFormat().backgroundBrush();
1960 if ( !block.blockFormat().backgroundImagePath().isEmpty() )
1961 {
1962 const QBrush backgroundImageBrush = createBrushForPath( context, block.blockFormat().backgroundImagePath() );
1963 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1964 backgroundBrush = backgroundImageBrush;
1965 }
1966
1967 context.painter()->setBrush( backgroundBrush );
1968 context.painter()->drawRect(
1969 QRectF( blockMetrics[blockIndex].backgroundXOffset, baseLineOffset - blockMaximumAscent, blockMetrics[blockIndex].backgroundWidth, blockMaximumDescent + blockMaximumAscent )
1970 );
1971 }
1972
1973 double xOffset = 0;
1974 int fragmentIndex = 0;
1975
1976 for ( const QgsTextFragment &fragment : block )
1977 {
1978 const double horizontalAdvance = metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );
1979 const double ascent = metrics.fragmentAscent( blockIndex, fragmentIndex, mode );
1980 const double descent = metrics.fragmentDescent( blockIndex, fragmentIndex, mode );
1981
1982 if ( fragment.characterFormat().hasBackground() )
1983 {
1984 const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
1985
1986 QBrush backgroundBrush = fragment.characterFormat().backgroundBrush();
1987 if ( !fragment.characterFormat().backgroundImagePath().isEmpty() )
1988 {
1989 const QBrush backgroundImageBrush = createBrushForPath( context, fragment.characterFormat().backgroundImagePath() );
1990 if ( backgroundImageBrush.style() == Qt::BrushStyle::TexturePattern )
1991 backgroundBrush = backgroundImageBrush;
1992 }
1993
1994 context.painter()->setBrush( backgroundBrush );
1995 context.painter()->drawRect( QRectF( blockMetrics[blockIndex].xOffset + xOffset, baseLineOffset + verticalAlignOffset + yOffset - ascent, horizontalAdvance, ascent + descent ) );
1996 }
1997
1998 xOffset += horizontalAdvance;
1999 fragmentIndex++;
2000 }
2001
2002 blockIndex++;
2003 }
2004
2005 context.painter()->setBrush( Qt::NoBrush );
2006
2007 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2008 context.painter()->rotate( -rotationDegrees );
2009 context.painter()->translate( -component.origin );
2010}
2011
2012void QgsTextRenderer::drawTextInternalHorizontal(
2013 QgsRenderContext &context,
2014 const QgsTextFormat &format,
2015 Qgis::TextComponents components,
2017 const Component &component,
2018 const QgsTextDocument &document,
2019 const QgsTextDocumentMetrics &metrics,
2020 double fontScale,
2021 const Qgis::TextHorizontalAlignment hAlignment,
2022 Qgis::TextVerticalAlignment vAlignment,
2023 double rotationDegrees
2024)
2025{
2026 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2027
2028 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Horizontal );
2029
2030 double targetWidth = 0.0;
2031 switch ( mode )
2032 {
2035 targetWidth = documentSize.width();
2036 break;
2037
2041 targetWidth = component.size.width();
2042 break;
2043 }
2044
2045 double verticalAlignOffset = 0;
2046
2047 if ( mode == Qgis::TextLayoutMode::Rectangle )
2048 {
2049 const double overallHeight = documentSize.height();
2050 switch ( vAlignment )
2051 {
2053 verticalAlignOffset = metrics.blockVerticalMargin( -1 );
2054 break;
2055
2057 verticalAlignOffset = ( component.size.height() - overallHeight ) * 0.5 + metrics.blockVerticalMargin( -1 );
2058 break;
2059
2061 verticalAlignOffset = ( component.size.height() - overallHeight ) + metrics.blockVerticalMargin( -1 );
2062 break;
2063 }
2064 }
2065 else if ( mode == Qgis::TextLayoutMode::Point )
2066 {
2067 verticalAlignOffset = -metrics.blockVerticalMargin( document.size() - 1 );
2068 }
2069
2070 // should we use text or paths for this render?
2071 const bool usePathsForText = usePathsToRender( context, format, document );
2072
2073 // TODO -- maybe we can avoid the nested vector? Need to confirm whether painter rotation & translation can be
2074 // done ONCE only, upfront
2075 std::unique_ptr< std::vector< DeferredRenderBlock > > deferredBlocks;
2076
2077 // Depending on format settings, we may need to render in multiple passes. Eg buffer than text, or shadow than text.
2078 // We try to avoid this if possible as it requires more work, and just do a single pass, rendering text directly as we go.
2079 // If we need to do multi-pass rendering then we'll calculate paths ONCE upfront and defer actually rendering these.
2080 const bool requiresMultiPassRendering
2081 = ( components & Qgis::TextComponent::Buffer && format.buffer().enabled() )
2083 if ( requiresMultiPassRendering )
2084 {
2085 deferredBlocks = std::make_unique< std::vector< DeferredRenderBlock > >();
2086 deferredBlocks->reserve( document.size() );
2087 }
2088
2089 if ( ( components & Qgis::TextComponent::Buffer ) || ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2090 {
2091 const QVector< BlockMetrics > blockMetrics = calculateBlockMetrics( document, metrics, mode, targetWidth, hAlignment );
2092
2093 if ( document.hasBackgrounds() )
2094 {
2095 renderDocumentBackgrounds( context, document, metrics, component, blockMetrics, mode, verticalAlignOffset, rotationDegrees );
2096 }
2097
2098 int blockIndex = 0;
2099 for ( const QgsTextBlock &block : document )
2100 {
2101 const double blockHeight = metrics.blockHeight( blockIndex );
2102
2103 DeferredRenderBlock *deferredBlock = nullptr;
2104 if ( requiresMultiPassRendering && deferredBlocks )
2105 {
2106 deferredBlocks->emplace_back( DeferredRenderBlock() );
2107 deferredBlock = &deferredBlocks->back();
2108 deferredBlock->fragments.reserve( block.size() );
2109 }
2110
2111 QgsScopedQPainterState painterState( context.painter() );
2113 context.painter()->translate( component.origin );
2114 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2115 context.painter()->rotate( rotationDegrees );
2116
2117 // apply to the mask painter the same transformations
2118 std::unique_ptr< QgsScopedQPainterState > maskPainterState;
2119 if ( maskPainter )
2120 {
2121 maskPainterState = std::make_unique< QgsScopedQPainterState>( maskPainter );
2122 maskPainter->translate( component.origin );
2123 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2124 maskPainter->rotate( rotationDegrees );
2125 }
2126
2127 const BlockMetrics thisBlockMetrics = blockMetrics[blockIndex];
2128 const double baseLineOffset = metrics.baselineOffset( blockIndex, mode );
2129
2130 const QPointF blockOrigin( thisBlockMetrics.xOffset, baseLineOffset + verticalAlignOffset );
2131 if ( deferredBlock )
2132 deferredBlock->origin = blockOrigin;
2133 else
2134 context.painter()->translate( blockOrigin );
2135 if ( maskPainter )
2136 maskPainter->translate( blockOrigin );
2137
2138 Component subComponent;
2139 subComponent.block = block;
2140 subComponent.blockIndex = blockIndex;
2141 subComponent.size = QSizeF( thisBlockMetrics.width, blockHeight );
2142 subComponent.offset = QPointF( 0.0, -metrics.ascentOffset() );
2143 subComponent.rotationRadians = -component.rotationRadians;
2144 subComponent.rotationOffsetRadians = 0.0;
2145 subComponent.extraWordSpacing = thisBlockMetrics.extraWordSpace * fontScale;
2146 subComponent.extraLetterSpacing = thisBlockMetrics.extraLetterSpace * fontScale;
2147 if ( deferredBlock )
2148 deferredBlock->component = subComponent;
2149
2150 // draw the mask below the text (for preview)
2151 if ( format.mask().enabled() )
2152 {
2153 QgsTextRenderer::drawMask( context, subComponent, format, metrics, mode );
2154 }
2155
2156 // if we are drawing both text + buffer, we'll need a path, as we HAVE to render buffers using paths
2157 const bool needsPaths = usePathsForText
2158 || ( ( components & Qgis::TextComponent::Buffer ) && format.buffer().enabled() )
2159 || ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() );
2160
2161 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2162 if ( mode == Qgis::TextLayoutMode::Labeling )
2163 {
2164 // label size has already been calculated using any symbology reference scale factor -- we need
2165 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2166 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2167 }
2168
2169 referenceScaleOverride.reset();
2170
2171 // now render the actual text
2173 {
2174 context.painter()->setCompositionMode( format.blendMode() );
2175 }
2176
2177 // scale for any print output or image saving @ specific dpi
2178 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2179
2180 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2181 context.painter()->setPen( Qt::NoPen );
2182 context.painter()->setBrush( Qt::NoBrush );
2183
2184 renderBlockHorizontal( block, blockIndex, metrics, context, format, context.painter(), needsPaths, fontScale, thisBlockMetrics.extraWordSpace, thisBlockMetrics.extraLetterSpace, mode, deferredBlock );
2185
2186 blockIndex++;
2187 }
2188 }
2189
2190 if ( deferredBlocks )
2191 {
2192 renderDeferredBlocks( context, format, components, *deferredBlocks, usePathsForText, fontScale, component, rotationDegrees );
2193 }
2194}
2195
2196void QgsTextRenderer::renderDeferredBlocks(
2197 QgsRenderContext &context,
2198 const QgsTextFormat &format,
2199 Qgis::TextComponents components,
2200 const std::vector< DeferredRenderBlock > &deferredBlocks,
2201 bool usePathsForText,
2202 double fontScale,
2203 const Component &component,
2204 double rotationDegrees
2205)
2206{
2207 if ( format.buffer().enabled() && ( components & Qgis::TextComponent::Buffer ) )
2208 {
2209 renderDeferredBuffer( context, format, components, deferredBlocks, fontScale, component, rotationDegrees );
2210 }
2211
2212 if ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2213 {
2214 renderDeferredShadowForText( context, format, deferredBlocks, fontScale, component, rotationDegrees );
2215 // TODO: there's an optimisation opportunity here -- if we are ALSO rendering the text component,
2216 // we could move the actual text rendering into renderDeferredShadowForText and use the same
2217 // QPicture as we used for the shadow. But we'd need to ensure that all the settings
2218 // which control whether text is rendered as text or paths also also considered.
2219 }
2220
2221 if ( components & Qgis::TextComponent::Text )
2222 {
2223 renderDeferredText( context, deferredBlocks, usePathsForText, fontScale, component, rotationDegrees );
2224 }
2225}
2226
2227void QgsTextRenderer::renderDeferredShadowForText(
2228 QgsRenderContext &context, const QgsTextFormat &format, const std::vector< DeferredRenderBlock > &deferredBlocks, double fontScale, const Component &component, double rotationDegrees
2229)
2230{
2231 QgsScopedQPainterState painterState( context.painter() );
2233 context.painter()->translate( component.origin );
2234 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2235 context.painter()->rotate( rotationDegrees );
2236
2237 context.painter()->setPen( Qt::NoPen );
2238 context.painter()->setBrush( Qt::NoBrush );
2239
2240 for ( const DeferredRenderBlock &block : deferredBlocks )
2241 {
2242 Component subComponent = block.component;
2243
2244 QPainter painter( &subComponent.picture );
2245 painter.setPen( Qt::NoPen );
2246 painter.setBrush( Qt::NoBrush );
2247 painter.scale( 1 / fontScale, 1 / fontScale );
2248
2249 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2250 {
2251 if ( !fragment.path.isEmpty() )
2252 {
2253 painter.setBrush( fragment.color );
2254 painter.drawPath( fragment.path );
2255 }
2256 else
2257 {
2258 painter.setPen( fragment.color );
2259 painter.setFont( fragment.font );
2260 painter.drawText( fragment.point, fragment.text );
2261 }
2262 }
2263 painter.end();
2264
2265 subComponent.pictureBuffer = 1.0; // no pen width to deal with, but we'll add 1 px for antialiasing
2266 subComponent.origin = QPointF( 0.0, 0.0 );
2267 const QRectF pictureBoundingRect = subComponent.picture.boundingRect();
2268 subComponent.size = pictureBoundingRect.size();
2269 subComponent.offset = QPointF( -pictureBoundingRect.left(), -pictureBoundingRect.height() - pictureBoundingRect.top() );
2270
2271 context.painter()->translate( block.origin );
2272 drawShadow( context, subComponent, format );
2273 context.painter()->translate( -block.origin );
2274 }
2275}
2276
2277void QgsTextRenderer::renderDeferredBuffer(
2278 QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponents components, const std::vector< DeferredRenderBlock > &deferredBlocks, double fontScale, const Component &component, double rotationDegrees
2279)
2280{
2281 QgsScopedQPainterState painterState( context.painter() );
2283
2284 // do we need a drop shadow effect on the buffer component? If so, we'll render the buffer to a QPicture first and then use this
2285 // to generate the shadow, and then render the QPicture as the buffer on top. If not, avoid the unwanted expense of the temporary QPicture
2286 // and render directly.
2287 const bool needsShadowOnBuffer = ( ( components & Qgis::TextComponent::Shadow ) && format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowBuffer );
2288 std::unique_ptr< QPicture > bufferPicture;
2289 std::unique_ptr< QPainter > bufferPainter;
2290 QPainter *prevPainter = context.painter();
2291 if ( needsShadowOnBuffer )
2292 {
2293 bufferPicture = std::make_unique< QPicture >();
2294 bufferPainter = std::make_unique< QPainter >( bufferPicture.get() );
2295 context.setPainter( bufferPainter.get() );
2296 }
2297
2298 std::unique_ptr< QgsPaintEffect > tmpEffect;
2299 if ( format.buffer().paintEffect() && format.buffer().paintEffect()->enabled() )
2300 {
2301 tmpEffect.reset( format.buffer().paintEffect()->clone() );
2302 tmpEffect->begin( context );
2303 }
2304
2305 QColor bufferColor = format.buffer().color();
2306 bufferColor.setAlphaF( format.buffer().opacity() );
2307 QPen pen( bufferColor );
2308 const QgsTextBufferSettings &buffer = format.buffer();
2309 const double penSize = buffer.sizeUnit() == Qgis::RenderUnit::Percentage ? context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() ) * buffer.size() / 100
2310 : context.convertToPainterUnits( buffer.size(), buffer.sizeUnit(), buffer.sizeMapUnitScale() );
2311 pen.setWidthF( penSize * fontScale );
2312 pen.setJoinStyle( buffer.joinStyle() );
2313 context.painter()->setPen( pen );
2314
2315 // honor pref for whether to fill buffer interior
2316 if ( !buffer.fillBufferInterior() )
2317 {
2318 bufferColor.setAlpha( 0 );
2319 }
2320 context.painter()->setBrush( bufferColor );
2321
2322 context.painter()->translate( component.origin );
2323 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2324 context.painter()->rotate( rotationDegrees );
2325
2327 {
2328 context.painter()->setCompositionMode( format.buffer().blendMode() );
2329 }
2330
2331 for ( const DeferredRenderBlock &block : deferredBlocks )
2332 {
2333 context.painter()->translate( block.origin );
2334 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2335 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2336 {
2337 context.painter()->drawPath( fragment.path );
2338 }
2339 context.painter()->scale( fontScale, fontScale );
2340 context.painter()->translate( -block.origin );
2341 }
2342
2343 if ( tmpEffect )
2344 {
2345 tmpEffect->end( context );
2346 }
2347
2348 if ( needsShadowOnBuffer && bufferPicture )
2349 {
2350 bufferPainter->end();
2351 bufferPainter.reset();
2352 context.setPainter( prevPainter );
2353
2354 QgsTextRenderer::Component bufferComponent = component;
2355 bufferComponent.origin = QPointF( 0.0, 0.0 );
2356 bufferComponent.picture = *bufferPicture;
2357 bufferComponent.pictureBuffer = penSize / 2.0;
2358 const QRectF bufferBoundingBox = bufferPicture->boundingRect();
2359 bufferComponent.size = bufferBoundingBox.size();
2360 bufferComponent.offset = QPointF( -bufferBoundingBox.left(), -bufferBoundingBox.height() - bufferBoundingBox.top() );
2361
2362 drawShadow( context, bufferComponent, format );
2363
2364 // also draw buffer
2366 {
2367 context.painter()->setCompositionMode( buffer.blendMode() );
2368 }
2369
2370 // scale for any print output or image saving @ specific dpi
2371 context.painter()->scale( component.dpiRatio, component.dpiRatio );
2372 QgsPainting::drawPicture( context.painter(), QPointF( 0, 0 ), *bufferPicture );
2373 }
2374}
2375
2376void QgsTextRenderer::renderDeferredText(
2377 QgsRenderContext &context, const std::vector< DeferredRenderBlock > &deferredBlocks, bool usePathsForText, double fontScale, const Component &component, double rotationDegrees
2378)
2379{
2380 QgsScopedQPainterState painterState( context.painter() );
2382 context.painter()->translate( component.origin );
2383 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2384 context.painter()->rotate( rotationDegrees );
2385
2386 context.painter()->setPen( Qt::NoPen );
2387 context.painter()->setBrush( Qt::NoBrush );
2388
2389 // draw the text
2390 for ( const DeferredRenderBlock &block : deferredBlocks )
2391 {
2392 context.painter()->translate( block.origin );
2393 context.painter()->scale( 1 / fontScale, 1 / fontScale );
2394
2395 for ( const DeferredRenderFragment &fragment : std::as_const( block.fragments ) )
2396 {
2397 if ( usePathsForText )
2398 {
2399 context.painter()->setBrush( fragment.color );
2400 context.painter()->drawPath( fragment.path );
2401 }
2402 else
2403 {
2404 context.painter()->setPen( fragment.color );
2405 context.painter()->setFont( fragment.font );
2406 context.painter()->drawText( fragment.point, fragment.text );
2407 }
2408 }
2409
2410 context.painter()->scale( fontScale, fontScale );
2411 context.painter()->translate( -block.origin );
2412 }
2413}
2414
2415void QgsTextRenderer::drawTextInternalVertical(
2416 QgsRenderContext &context,
2417 const QgsTextFormat &format,
2418 Qgis::TextComponents components,
2420 const QgsTextRenderer::Component &component,
2421 const QgsTextDocument &document,
2422 const QgsTextDocumentMetrics &metrics,
2423 double fontScale,
2426 double rotationDegrees
2427)
2428{
2429 QPainter *maskPainter = context.maskPainter( context.currentMaskId() );
2430 const QStringList textLines = document.toPlainText();
2431
2432 std::optional< QgsScopedRenderContextReferenceScaleOverride > referenceScaleOverride;
2433 if ( mode == Qgis::TextLayoutMode::Labeling )
2434 {
2435 // label size has already been calculated using any symbology reference scale factor -- we need
2436 // to temporarily remove the reference scale here or we'll be applying the scaling twice
2437 referenceScaleOverride.emplace( QgsScopedRenderContextReferenceScaleOverride( context, -1.0 ) );
2438 }
2439
2440 if ( metrics.isNullFontSize() )
2441 return;
2442
2443 referenceScaleOverride.reset();
2444
2445 const QSizeF documentSize = metrics.documentSize( mode, Qgis::TextOrientation::Vertical );
2446 const double actualTextWidth = documentSize.width();
2447 double textRectWidth = 0.0;
2448
2449 switch ( mode )
2450 {
2453 textRectWidth = actualTextWidth;
2454 break;
2455
2459 textRectWidth = component.size.width();
2460 break;
2461 }
2462
2463 int maxLineLength = 0;
2464 for ( const QString &line : std::as_const( textLines ) )
2465 {
2466 maxLineLength = std::max( maxLineLength, static_cast<int>( line.length() ) );
2467 }
2468
2469 const double actualLabelHeight = documentSize.height();
2470 int blockIndex = 0;
2471
2472 bool adjustForAlignment = hAlignment != Qgis::TextHorizontalAlignment::Left && ( mode != Qgis::TextLayoutMode::Labeling || textLines.size() > 1 );
2473
2474 for ( const QgsTextBlock &block : document )
2475 {
2476 QgsScopedQPainterState painterState( context.painter() );
2478
2479 context.painter()->translate( component.origin );
2480 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2481 context.painter()->rotate( rotationDegrees );
2482
2483 // apply to the mask painter the same transformations
2484 std::unique_ptr<QgsScopedQPainterState > maskPainterState;
2485 if ( maskPainter )
2486 {
2487 maskPainterState = std::make_unique< QgsScopedQPainterState >( maskPainter );
2488 maskPainter->translate( component.origin );
2489 if ( !qgsDoubleNear( rotationDegrees, 0.0 ) )
2490 maskPainter->rotate( rotationDegrees );
2491 }
2492
2493 const double blockMaximumCharacterWidth = metrics.blockMaximumCharacterWidth( blockIndex );
2494
2495 // figure x offset of multiple lines
2496 double xOffset = metrics.verticalOrientationXOffset( blockIndex );
2497 if ( adjustForAlignment )
2498 {
2499 double hAlignmentOffset = 0;
2500 switch ( hAlignment )
2501 {
2503 hAlignmentOffset = ( textRectWidth - actualTextWidth ) * 0.5;
2504 break;
2505
2507 hAlignmentOffset = textRectWidth - actualTextWidth;
2508 break;
2509
2512 break;
2513 }
2514
2515 switch ( mode )
2516 {
2521 xOffset += hAlignmentOffset;
2522 break;
2523
2525 break;
2526 }
2527 }
2528
2529 double yOffset = 0.0;
2530 switch ( mode )
2531 {
2534 {
2535 if ( rotationDegrees >= -405 && rotationDegrees < -180 )
2536 {
2537 yOffset = 0;
2538 }
2539 else if ( rotationDegrees >= 0 && rotationDegrees < 45 )
2540 {
2541 xOffset -= actualTextWidth;
2542 yOffset = -actualLabelHeight + metrics.blockMaximumDescent( blockIndex );
2543 }
2544 }
2545 else
2546 {
2547 yOffset = -actualLabelHeight;
2548 }
2549 break;
2550
2552 yOffset = -actualLabelHeight;
2553 break;
2554
2558 yOffset = 0;
2559 break;
2560 }
2561
2562 context.painter()->translate( QPointF( xOffset, yOffset ) );
2563
2564 double currentBlockYOffset = 0;
2565 int fragmentIndex = 0;
2566 for ( const QgsTextFragment &fragment : block )
2567 {
2568 QgsScopedQPainterState fragmentPainterState( context.painter() );
2569
2570 // apply some character replacement to draw symbols in vertical presentation
2571 const QString line = QgsStringUtils::substituteVerticalCharacters( fragment.text() );
2572
2573 const QFont fragmentFont = metrics.fragmentFont( blockIndex, fragmentIndex );
2574
2575 QFontMetricsF fragmentMetrics( fragmentFont );
2576
2577 const double letterSpacing = fragmentFont.letterSpacing() / fontScale;
2578 const double labelHeight = fragmentMetrics.ascent() / fontScale + ( fragmentMetrics.ascent() / fontScale + letterSpacing ) * ( line.length() - 1 );
2579
2580 Component subComponent;
2581 subComponent.block = QgsTextBlock( fragment );
2582 subComponent.blockIndex = blockIndex;
2583 subComponent.firstFragmentIndex = fragmentIndex;
2584 subComponent.size = QSizeF( blockMaximumCharacterWidth, labelHeight + fragmentMetrics.descent() / fontScale );
2585 subComponent.offset = QPointF( 0.0, currentBlockYOffset );
2586 subComponent.rotationRadians = -component.rotationRadians;
2587 subComponent.rotationOffsetRadians = 0.0;
2588
2589 // draw the mask below the text (for preview)
2590 if ( format.mask().enabled() )
2591 {
2592 // WARNING: totally broken! (has been since mask was introduced)
2593#if 0
2594 QgsTextRenderer::drawMask( context, subComponent, format );
2595#endif
2596 }
2597
2598 if ( components & Qgis::TextComponent::Buffer )
2599 {
2600 currentBlockYOffset += QgsTextRenderer::drawBuffer( context, subComponent, format, metrics, mode );
2601 }
2602 if ( ( components & Qgis::TextComponent::Text ) || ( components & Qgis::TextComponent::Shadow ) )
2603 {
2604 // draw text, QPainterPath method
2605 QPainterPath path;
2606 path.setFillRule( Qt::WindingFill );
2607 const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
2608 double partYOffset = 0.0;
2609 for ( const QString &part : parts )
2610 {
2611 double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / fontScale - letterSpacing ) ) / 2;
2612 partYOffset += fragmentMetrics.ascent() / fontScale;
2613 path.addText( partXOffset * fontScale, partYOffset * fontScale, fragmentFont, part );
2614 partYOffset += letterSpacing;
2615 }
2616
2617 // store text's drawing in QPicture for drop shadow call
2618 QPicture textPict;
2619 QPainter textp;
2620 textp.begin( &textPict );
2621 textp.setPen( Qt::NoPen );
2622 QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
2623 textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
2624 textp.setBrush( textColor );
2625 textp.scale( 1 / fontScale, 1 / fontScale );
2626 textp.drawPath( path );
2627
2628 // TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
2629 // e.g. some capitalization options, but not others
2630 //textp.setFont( tmpLyr.textFont );
2631 //textp.setPen( tmpLyr.textColor );
2632 //textp.drawText( 0, 0, component.text() );
2633 textp.end();
2634
2635 if ( format.shadow().enabled() && format.shadow().shadowPlacement() == QgsTextShadowSettings::ShadowText )
2636 {
2637 subComponent.picture = textPict;
2638 subComponent.pictureBuffer = 0.0; // no pen width to deal with
2639 subComponent.origin = QPointF( 0.0, currentBlockYOffset );
2640 const double prevY = subComponent.offset.y();
2641 subComponent.offset = QPointF( 0, -subComponent.size.height() );
2642 subComponent.useOrigin = true;
2643 QgsTextRenderer::drawShadow( context, subComponent, format );
2644 subComponent.useOrigin = false;
2645 subComponent.offset = QPointF( 0, prevY );
2646 }
2647
2648 // paint the text
2650 {
2651 context.painter()->setCompositionMode( format.blendMode() );
2652 }
2653
2654 // scale for any print output or image saving @ specific dpi
2655 context.painter()->scale( subComponent.dpiRatio, subComponent.dpiRatio );
2656
2657 // TODO -- this should respect the context's TextRenderFormat
2658 // draw outlined text
2659 context.painter()->translate( 0, currentBlockYOffset );
2661 context.painter()->drawPicture( 0, 0, textPict );
2662 currentBlockYOffset += partYOffset;
2663 }
2664 fragmentIndex++;
2665 }
2666
2667 blockIndex++;
2668 }
2669}
2670
2672{
2674 return 1.0;
2675
2676 const double pixelSize = context.convertToPainterUnits( format.size(), format.sizeUnit(), format.sizeMapUnitScale() );
2677
2678 // THESE THRESHOLDS MAY NEED TWEAKING!
2679
2680 // NOLINTBEGIN(bugprone-branch-clone)
2681
2682 // for small font sizes we need to apply a growth scaling workaround designed to stablise the rendering of small font sizes
2683 // we scale the painter up so that we render small text at 200 pixel size and let the painter scaling handle making it the correct size
2684 if ( pixelSize < 50 )
2685 return 200 / pixelSize;
2686 //... but for large font sizes we might run into https://bugreports.qt.io/browse/QTBUG-98778, which messes up the spacing between words for large fonts!
2687 // 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
2688 else if ( pixelSize > 200 )
2689 return 200 / pixelSize;
2690 else
2691 return 1.0;
2692
2693 // NOLINTEND(bugprone-branch-clone)
2694}
@ ForceVector
Always force vector-based rendering, even when the result will be visually different to a raster-base...
Definition qgis.h:2919
TextAnchorPoint
Anchor point of label text.
Definition qgis.h:1495
TextLayoutMode
Text layout modes.
Definition qgis.h:3126
@ Labeling
Labeling-specific layout mode.
Definition qgis.h:3129
@ Point
Text at point of origin layout mode.
Definition qgis.h:3128
@ RectangleAscentBased
Similar to Rectangle mode, but uses ascents only when calculating font and line heights.
Definition qgis.h:3131
@ RectangleCapHeightBased
Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line ...
Definition qgis.h:3130
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:3127
QFlags< TextRendererFlag > TextRendererFlags
Definition qgis.h:3649
TextOrientation
Text orientations.
Definition qgis.h:3111
@ Vertical
Vertically oriented text.
Definition qgis.h:3113
@ RotationBased
Horizontally or vertically oriented text based on rotation (only available for map labeling).
Definition qgis.h:3114
@ Horizontal
Horizontally oriented text.
Definition qgis.h:3112
@ Round
Use rounded joins.
Definition qgis.h:2294
@ Normal
Adjacent characters are positioned in the standard way for text in the writing system in use.
Definition qgis.h:3203
@ SubScript
Characters are placed below the base line for normal text.
Definition qgis.h:3205
@ SuperScript
Characters are placed above the base line for normal text.
Definition qgis.h:3204
@ PreferText
Render text as text objects, unless doing so results in rendering artifacts or poor quality rendering...
Definition qgis.h:3050
@ AlwaysOutlines
Always render text using path objects (AKA outlines/curves). This setting guarantees the best quality...
Definition qgis.h:3044
@ AlwaysText
Always render text as text objects. While this mode preserves text objects as text for post-processin...
Definition qgis.h:3047
RenderUnit
Rendering size units.
Definition qgis.h:5758
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size).
Definition qgis.h:5762
@ Unknown
Mixed or unknown units.
Definition qgis.h:5765
@ MapUnits
Map units.
Definition qgis.h:5760
@ Pixels
Pixels.
Definition qgis.h:5761
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2978
@ RenderBlocking
Render and load remote sources in the same thread to ensure rendering remote sources (svg and images)...
Definition qgis.h:2975
TextVerticalAlignment
Text vertical alignment.
Definition qgis.h:3186
@ Bottom
Align to bottom.
Definition qgis.h:3189
@ Top
Align to top.
Definition qgis.h:3187
@ VerticalCenter
Center align.
Definition qgis.h:3188
QFlags< TextComponent > TextComponents
Text components.
Definition qgis.h:3156
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3167
@ Justify
Justify align.
Definition qgis.h:3171
@ Center
Center align.
Definition qgis.h:3169
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3646
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
Definition qgis.h:3228
TextComponent
Text components.
Definition qgis.h:3143
@ Shadow
Drop shadow.
Definition qgis.h:3147
@ Buffer
Buffer component.
Definition qgis.h:3145
@ Text
Text component.
Definition qgis.h:3144
@ Background
Background shape.
Definition qgis.h:3146
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
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.
QgsFillSymbol * clone() const override
Returns a deep copy of this symbol.
Does vector analysis using the GEOS library and handles import, export, and exception handling.
Definition qgsgeos.h:192
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, double targetDpi=96, int frameNumber=-1, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
Line string geometry type, with support for z-dimension and m-values.
static std::unique_ptr< QgsLineString > fromQPolygonF(const QPolygonF &polygon)
Returns a new linestring from a QPolygonF polygon input.
Struct for storing maximum and minimum scales for measurements in map units.
QgsMarkerSymbol * clone() const override
Returns a deep copy of this symbol.
bool enabled() const
Returns whether the effect is enabled.
virtual QgsPaintEffect * clone() const =0
Duplicates an effect by creating a deep copy of the effect.
static void applyScaleFixForQPictureDpi(QPainter *painter)
Applies a workaround to a painter to avoid an issue with incorrect scaling when drawing QPictures.
static void drawPicture(QPainter *painter, const QPointF &point, const QPicture &picture)
Draws a picture onto a painter, correctly applying workarounds to avoid issues with incorrect scaling...
static QStringList splitToGraphemes(const QString &text)
Splits a text string to a list of graphemes, which are the smallest allowable character divisions in ...
Contains precalculated properties regarding text metrics for text to be rendered at a later stage.
void setGraphemeFormats(const QVector< QgsTextCharacterFormat > &formats)
Sets the character formats associated with the text graphemes().
bool hasActiveProperties() const final
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.
void setScaleFactor(double factor)
Sets the scaling factor for the render to convert painter units to physical sizes.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
QgsExpressionContext & expressionContext()
Gets the expression context.
bool isGuiPreview() const
Returns the Gui preview mode.
Qgis::TextRenderFormat textRenderFormat() const
Returns the text render format, which dictates how text is rendered (e.g.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
QPainter * maskPainter(int id=0)
Returns a mask QPainter for the render operation.
void setMapToPixel(const QgsMapToPixel &mtp)
Sets the context's map to pixel transform, which transforms between map coordinates and device coordi...
int currentMaskId() const
Returns the current mask id, which can be used with maskPainter().
Qgis::RasterizedRenderingPolicy rasterizedRenderingPolicy() const
Returns the policy controlling when rasterisation of content during renders is permitted.
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
Qgis::RenderContextFlags flags() const
Returns combination of flags used for rendering.
static QString substituteVerticalCharacters(QString string)
Returns a string with characters having vertical representation form substituted.
static std::unique_ptr< 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.
QgsMapUnitScale strokeWidthMapUnitScale() const
Returns the map unit scale object for the shape stroke width.
RotationType rotationType() const
Returns the method used for rotating the background shape.
QString svgFile() const
Returns the absolute path to the background SVG file, if set.
QSizeF size() const
Returns the size of the background shape.
QSizeF radii() const
Returns the radii used for rounding the corners of shapes.
QgsMapUnitScale radiiMapUnitScale() const
Returns the map unit scale object for the shape radii.
Qgis::RenderUnit radiiUnit() const
Returns the units used for the shape's radii.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the background shape.
@ SizeBuffer
Shape size is determined by adding a buffer margin around text.
bool enabled() const
Returns whether the background is enabled.
double opacity() const
Returns the background shape's opacity.
double rotation() const
Returns the rotation for the background shape, in degrees clockwise.
QColor fillColor() const
Returns the color used for filing the background shape.
SizeType sizeType() const
Returns the method used to determine the size of the background shape (e.g., fixed size or buffer aro...
Qgis::RenderUnit strokeWidthUnit() const
Returns the units used for the shape's stroke width.
ShapeType type() const
Returns the type of background shape (e.g., square, ellipse, SVG).
double strokeWidth() const
Returns the width of the shape's stroke (stroke).
@ ShapeSquare
Square - buffered sizes only.
Qgis::RenderUnit offsetUnit() const
Returns the units used for the shape's offset.
QColor strokeColor() const
Returns the color used for outlining the background shape.
QgsFillSymbol * fillSymbol() const
Returns the fill symbol to be rendered in the background.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the shape size.
Qgis::RenderUnit sizeUnit() const
Returns the units used for the shape's size.
@ RotationOffset
Shape rotation is offset from text rotation.
@ RotationFixed
Shape rotation is a fixed angle.
QgsMarkerSymbol * markerSymbol() const
Returns the marker symbol to be rendered in the background.
const QgsPaintEffect * paintEffect() const
Returns the current paint effect for the background shape.
QgsMapUnitScale offsetMapUnitScale() const
Returns the map unit scale object for the shape offset.
QPointF offset() const
Returns the offset used for drawing the background shape.
Represents a block of text consisting of one or more QgsTextFragment objects.
QString toPlainText() const
Converts the block to plain text.
Qgis::RenderUnit sizeUnit() const
Returns the units for the buffer size.
Qt::PenJoinStyle joinStyle() const
Returns the buffer join style.
double size() const
Returns the size of the buffer.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the buffer size.
bool enabled() const
Returns whether the buffer is enabled.
double opacity() const
Returns the buffer opacity.
bool fillBufferInterior() const
Returns whether the interior of the buffer will be filled in.
const QgsPaintEffect * paintEffect() const
Returns the current paint effect for the buffer.
QColor color() const
Returns the color of the buffer.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the buffer.
Stores information relating to individual character formatting.
void updateFontForFormat(QFont &font, const QgsRenderContext &context, double scaleFactor=1.0) const
Updates the specified font in place, applying character formatting options which are applicable on a ...
QColor textColor() const
Returns the character's text color, or an invalid color if no color override is set and the default f...
QString backgroundImagePath() const
Returns the path for the image to be used for rendering the background of the fragment.
QBrush backgroundBrush() const
Returns the brush used for rendering the background of the fragment.
QString imagePath() const
Returns the path to the image to render, if the format applies to a document image fragment.
Qgis::TextCharacterVerticalAlignment verticalAlignment() const
Returns the format vertical alignment.
bool hasVerticalAlignmentSet() const
Returns true if the format has an explicit vertical alignment set.
double fontPointSize() const
Returns the font point size, or -1 if the font size is not set and should be inherited.
bool hasBackground() const
Returns true if the fragment has a background set.
Contains pre-calculated metrics of a QgsTextDocument.
double verticalOrientationXOffset(int blockIndex) const
Returns the vertical orientation x offset for the specified block.
double fragmentVerticalOffset(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the vertical offset from a text block's baseline which should be applied to the fragment at t...
double blockMaximumDescent(int blockIndex) const
Returns the maximum descent encountered in the specified block.
double fragmentDescent(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the descent of the fragment at the specified block and fragment index.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
double blockRightMargin(int blockIndex) const
Returns the margin for the right side of the specified block index.
static QgsTextDocumentMetrics calculateMetrics(const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor=1.0, const QgsTextDocumentRenderContext &documentContext=QgsTextDocumentRenderContext())
Returns precalculated text metrics for a text document, when rendered using the given base format and...
QFont fragmentFont(int blockIndex, int fragmentIndex) const
Returns the calculated font for the fragment at the specified block and fragment indices.
double blockMaximumCharacterWidth(int blockIndex) const
Returns the maximum character width for the specified block.
double baselineOffset(int blockIndex, Qgis::TextLayoutMode mode) const
Returns the offset from the top of the document to the text baseline for the given block index.
double fragmentFixedHeight(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the fixed height of the fragment at the specified block and fragment index,...
double blockLeftMargin(int blockIndex) const
Returns the margin for the left side of the specified block index.
double blockMaximumAscent(int blockIndex) const
Returns the maximum ascent encountered in the specified block.
double fragmentAscent(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the ascent of the fragment at the specified block and fragment index.
double blockHeight(int blockIndex) const
Returns the height of the block at the specified index.
double fragmentHorizontalAdvance(int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode) const
Returns the horizontal advance of the fragment at the specified block and fragment index.
bool isNullFontSize() const
Returns true if the metrics could not be calculated because the text format has a null font size.
const QgsTextDocument & document() const
Returns the document associated with the calculated metrics.
double blockWidth(int blockIndex) const
Returns the width of the block at the specified index.
double ascentOffset() const
Returns the ascent offset of the first block in the document.
double blockVerticalMargin(int blockIndex) const
Returns the vertical margin for the specified block index.
Encapsulates the context in which a text document is to be rendered.
void setFlags(Qgis::TextRendererFlags flags)
Sets associated text renderer flags.
void setMaximumWidth(double width)
Sets the maximum width (in painter units) for rendered text.
Represents a document consisting of one or more QgsTextBlock objects.
QStringList toPlainText() const
Returns a list of plain text lines of text representing the document.
int size() const
Returns the number of blocks in the document.
void append(const QgsTextBlock &block)
Appends a block to the document.
static QgsTextDocument fromTextAndFormat(const QStringList &lines, const QgsTextFormat &format)
Constructor for QgsTextDocument consisting of a set of lines, respecting settings from a text format.
void applyCapitalization(Qgis::Capitalization capitalization)
Applies a capitalization style to the document's text.
bool hasBackgrounds() const
Returns true if any blocks or fragments in the document have background brushes set.
Container for all settings relating to text rendering.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the size.
void updateDataDefinedProperties(QgsRenderContext &context)
Updates the format by evaluating current values of data defined properties.
QgsPropertyCollection & dataDefinedProperties()
Returns a reference to the format's property collection, used for data defined overrides.
QFont scaledFont(const QgsRenderContext &context, double scaleFactor=1.0, bool *isZeroSize=nullptr) const
Returns a font with the size scaled to match the format's size settings (including units and map unit...
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the text.
Qgis::Capitalization capitalization() const
Returns the text capitalization style.
QgsTextMaskSettings & mask()
Returns a reference to the masking settings.
QgsTextBackgroundSettings & background()
Returns a reference to the text background settings.
Qgis::RenderUnit sizeUnit() const
Returns the units for the size of rendered text.
double opacity() const
Returns the text's opacity.
Qgis::TextOrientation orientation() const
Returns the orientation of the text.
double size() const
Returns the size for rendered text.
QgsTextShadowSettings & shadow()
Returns a reference to the text drop shadow settings.
QColor color() const
Returns the color that text will be rendered in.
QFont font() const
Returns the font used for rendering text.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.
QString text() const
Returns the text content of the fragment.
const QgsTextCharacterFormat & characterFormat() const
Returns the character formatting for the fragment.
bool isImage() const
Returns true if the fragment represents an image.
bool isWhitespace() const
Returns true if the fragment consists of just whitespace characters, and does not contain any content...
Qgis::RenderUnit sizeUnit() const
Returns the units for the buffer size.
QgsMapUnitScale sizeMapUnitScale() const
Returns the map unit scale object for the buffer size.
double size() const
Returns the size of the buffer.
QgsPaintEffect * paintEffect() const
Returns the current paint effect for the mask.
double opacity() const
Returns the mask's opacity.
bool enabled() const
Returns whether the mask is enabled.
Qt::PenJoinStyle joinStyle() const
Returns the buffer join style.
Contains placement information for a single grapheme in a curved text layout.
@ RespectPainterOrientation
Curved text will be placed respecting the painter orientation, and the actual line direction will be ...
static std::unique_ptr< CurvePlacementProperties > generateCurvedTextPlacement(const QgsPrecalculatedTextMetrics &metrics, const QPolygonF &line, double offsetAlongLine, LabelLineDirection direction=RespectPainterOrientation, double maxConcaveAngle=-1, double maxConvexAngle=-1, Qgis::CurvedTextFlags flags=Qgis::CurvedTextFlags(), Qgis::TextAnchorPoint textAnchor=Qgis::TextAnchorPoint::StartOfText)
Calculates curved text placement properties.
static Qgis::TextVerticalAlignment convertQtVAlignment(Qt::Alignment alignment)
Converts a Qt vertical alignment flag to a Qgis::TextVerticalAlignment value.
static double textWidth(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, QFontMetricsF *fontMetrics=nullptr)
Returns the width of a text based on a given format.
static void drawDocument(const QRectF &rect, const QgsTextFormat &format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment horizontalAlignment=Qgis::TextHorizontalAlignment::Left, Qgis::TextVerticalAlignment verticalAlignment=Qgis::TextVerticalAlignment::Top, double rotation=0, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags())
Draws a text document within a rectangle using the specified settings.
static int sizeToPixel(double size, const QgsRenderContext &c, Qgis::RenderUnit unit, const QgsMapUnitScale &mapUnitScale=QgsMapUnitScale())
Calculates pixel size (considering output size should be in pixel or map units, scale factors and opt...
static Q_DECL_DEPRECATED void drawPart(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, Qgis::TextComponent part, bool drawAsOutlines=true)
Draws a single component of rendered text using the specified settings.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
static bool textRequiresWrapping(const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format)
Returns true if the specified text requires line wrapping in order to fit within the specified width ...
static QFontMetricsF fontMetrics(QgsRenderContext &context, const QgsTextFormat &format, double scaleFactor=1.0)
Returns the font metrics for the given text format, when rendered in the specified render context.
static double calculateScaleFactorForFormat(const QgsRenderContext &context, const QgsTextFormat &format)
Returns the scale factor used for upscaling font sizes and downscaling destination painter devices.
static QStringList wrappedText(const QgsRenderContext &context, const QString &text, double width, const QgsTextFormat &format)
Wraps a text string to multiple lines, such that each individual line will fit within the specified w...
static void drawTextOnLine(const QPolygonF &line, const QString &text, QgsRenderContext &context, const QgsTextFormat &format, double offsetAlongLine=0, double offsetFromLine=0, Qgis::CurvedTextFlags flags=Qgis::CurvedTextFlag::UseBaselinePlacement|Qgis::CurvedTextFlag::TruncateStringWhenLineIsTooShort, Qgis::TextAnchorPoint textAnchor=Qgis::TextAnchorPoint::StartOfText)
Draws text along a line using the specified settings.
static double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Point, QFontMetricsF *fontMetrics=nullptr, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), double maxLineWidth=0)
Returns the height of a text based on a given format.
static constexpr double SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR
Scale factor to use for super or subscript text which doesn't have an explicit font size set.
static Qgis::TextHorizontalAlignment convertQtHAlignment(Qt::Alignment alignment)
Converts a Qt horizontal alignment flag to a Qgis::TextHorizontalAlignment value.
static void drawDocumentOnLine(const QPolygonF &line, const QgsTextFormat &format, const QgsTextDocument &document, QgsRenderContext &context, double offsetAlongLine=0, double offsetFromLine=0, Qgis::CurvedTextFlags flags=Qgis::CurvedTextFlag::UseBaselinePlacement|Qgis::CurvedTextFlag::TruncateStringWhenLineIsTooShort, Qgis::TextAnchorPoint textAnchor=Qgis::TextAnchorPoint::StartOfText)
Draws a text document along a line using the specified settings.
int offsetAngle() const
Returns the angle for offsetting the position of the shadow from the text.
bool enabled() const
Returns whether the shadow is enabled.
int scale() const
Returns the scaling used for the drop shadow (in percentage of original size).
Qgis::RenderUnit offsetUnit() const
Returns the units used for the shadow's offset.
void setShadowPlacement(QgsTextShadowSettings::ShadowPlacement placement)
Sets the placement for the drop shadow.
double opacity() const
Returns the shadow's opacity.
QgsMapUnitScale blurRadiusMapUnitScale() const
Returns the map unit scale object for the shadow blur radius.
QColor color() const
Returns the color of the drop shadow.
@ ShadowBuffer
Draw shadow under buffer.
@ ShadowShape
Draw shadow under background shape.
@ ShadowLowest
Draw shadow below all text components.
@ ShadowText
Draw shadow under text.
QgsTextShadowSettings::ShadowPlacement shadowPlacement() const
Returns the placement for the drop shadow.
Qgis::RenderUnit blurRadiusUnit() const
Returns the units used for the shadow's blur radius.
double offsetDistance() const
Returns the distance for offsetting the position of the shadow from the text.
QPainter::CompositionMode blendMode() const
Returns the blending mode used for drawing the drop shadow.
QgsMapUnitScale offsetMapUnitScale() const
Returns the map unit scale object for the shadow offset distance.
bool blurAlphaOnly() const
Returns whether only the alpha channel for the shadow will be blurred.
bool offsetGlobal() const
Returns true if the global shadow offset will be used.
double blurRadius() const
Returns the blur radius for the shadow.
static Q_INVOKABLE QString encodeUnit(Qgis::DistanceUnit unit)
Encodes a distance unit to a string.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored).
Contains geos related utilities and functions.
Definition qgsgeos.h:129
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 BUILTIN_UNREACHABLE
Definition qgis.h:8415
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7743
const char * finder(const char *name)
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30