QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgstextdocumentmetrics.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstextdocumentmetrics.cpp
3 -----------------
4 begin : September 2022
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 ***************************************************************************/
16#include "qgis.h"
17#include "qgsstringutils.h"
18#include "qgstextblock.h"
19#include "qgstextfragment.h"
20#include "qgstextformat.h"
21#include "qgstextdocument.h"
22#include "qgsrendercontext.h"
23#include "qgstextrenderer.h"
24
25#include <QFontMetricsF>
26
27// to match Qt behavior in QTextLine::draw
30
31QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor )
32{
34
35 const QFont font = format.scaledFont( context, scaleFactor, &res.mIsNullSize );
36 if ( res.isNullFontSize() )
37 return res;
38
39 // for absolute line heights
40 const double lineHeightPainterUnits = context.convertToPainterUnits( format.lineHeight(), format.lineHeightUnit() );
41
42 double width = 0;
43 double heightLabelMode = 0;
44 double heightPointRectMode = 0;
45 double heightCapHeightMode = 0;
46 double heightAscentMode = 0;
47 const int blockSize = document.size();
48 res.mFragmentFonts.reserve( blockSize );
49 double currentLabelBaseline = 0;
50 double currentPointBaseline = 0;
51 double currentRectBaseline = 0;
52 double currentCapHeightBasedBaseline = 0;
53 double currentAscentBasedBaseline = 0;
54 double lastLineLeading = 0;
55
56 double heightVerticalOrientation = 0;
57
58 QVector < double > blockVerticalLineSpacing;
59
60 double outerXMin = 0;
61 double outerXMax = 0;
62 double outerYMinLabel = 0;
63 double outerYMaxLabel = 0;
64
65 for ( int blockIndex = 0; blockIndex < blockSize; blockIndex++ )
66 {
67 const QgsTextBlock &block = document.at( blockIndex );
68
69 double blockWidth = 0;
70 double blockXMax = 0;
71 double blockYMaxAdjustLabel = 0;
72
73 double blockHeightUsingAscentDescent = 0;
74 double blockHeightUsingLineSpacing = 0;
75 double blockHeightVerticalOrientation = 0;
76
77 double blockHeightUsingAscentAccountingForVerticalOffset = 0;
78
79 const int fragmentSize = block.size();
80
81 double maxBlockAscent = 0;
82 double maxBlockDescent = 0;
83 double maxLineSpacing = 0;
84 double maxBlockLeading = 0;
85 double maxBlockMaxWidth = 0;
86 double maxBlockCapHeight = 0;
87
88 QList< double > fragmentVerticalOffsets;
89 fragmentVerticalOffsets.reserve( fragmentSize );
90
91 QList< QFont > fragmentFonts;
92 fragmentFonts.reserve( fragmentSize );
93 QList< double >fragmentHorizontalAdvance;
94 fragmentHorizontalAdvance.reserve( fragmentSize );
95
96 QFont previousNonSuperSubScriptFont;
97
98 for ( int fragmentIndex = 0; fragmentIndex < fragmentSize; ++fragmentIndex )
99 {
100 const QgsTextFragment &fragment = block.at( fragmentIndex );
101 const QgsTextCharacterFormat &fragmentFormat = fragment.characterFormat();
102
103 double fragmentHeightForVerticallyOffsetText = 0;
104 double fragmentYMaxAdjust = 0;
105
106 QFont updatedFont = font;
107 fragmentFormat.updateFontForFormat( updatedFont, context, scaleFactor );
108
109 QFontMetricsF fm( updatedFont );
110
111 if ( fragmentIndex == 0 )
112 previousNonSuperSubScriptFont = updatedFont;
113
114 double fragmentVerticalOffset = 0;
115 if ( fragmentFormat.hasVerticalAlignmentSet() )
116 {
117 switch ( fragmentFormat.verticalAlignment() )
118 {
120 previousNonSuperSubScriptFont = updatedFont;
121 break;
122
124 {
125 const QFontMetricsF previousFM( previousNonSuperSubScriptFont );
126
127 if ( fragmentFormat.fontPointSize() < 0 )
128 {
129 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
130 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
131 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
132 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
133 // which we should respect
134 updatedFont.setPixelSize( static_cast< int >( std::round( updatedFont.pixelSize() * QgsTextRenderer::SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
135 fm = QFontMetricsF( updatedFont );
136 }
137
138 // to match Qt behavior in QTextLine::draw
139 fragmentVerticalOffset = -( previousFM.ascent() + previousFM.descent() ) * SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
140
141 // note -- this should really be fm.ascent(), not fm.capHeight() -- but in practice the ascent of most fonts is too large
142 // and causes unnecessarily large bounding boxes of vertically offset text!
143 fragmentHeightForVerticallyOffsetText = -fragmentVerticalOffset + fm.capHeight() / scaleFactor;
144 break;
145 }
146
148 {
149 const QFontMetricsF previousFM( previousNonSuperSubScriptFont );
150
151 if ( fragmentFormat.fontPointSize() < 0 )
152 {
153 // see above!!
154 updatedFont.setPixelSize( static_cast< int>( std::round( updatedFont.pixelSize() * QgsTextRenderer::SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
155 fm = QFontMetricsF( updatedFont );
156 }
157
158 // to match Qt behavior in QTextLine::draw
159 fragmentVerticalOffset = ( previousFM.ascent() + previousFM.descent() ) * SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
160
161 fragmentYMaxAdjust = fragmentVerticalOffset + fm.descent() / scaleFactor;
162 break;
163 }
164 }
165 }
166 else
167 {
168 previousNonSuperSubScriptFont = updatedFont;
169 }
170 fragmentVerticalOffsets << fragmentVerticalOffset;
171
172 const double fragmentWidth = fm.horizontalAdvance( fragment.text() ) / scaleFactor;
173
174 fragmentHorizontalAdvance << fragmentWidth;
175
176 const double fragmentHeightUsingAscentDescent = ( fm.ascent() + fm.descent() ) / scaleFactor;
177 const double fragmentHeightUsingLineSpacing = fm.lineSpacing() / scaleFactor;
178
179 blockWidth += fragmentWidth;
180 blockXMax += fragmentWidth;
181 blockHeightUsingAscentDescent = std::max( blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent );
182
183 blockHeightUsingLineSpacing = std::max( blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
184 maxBlockAscent = std::max( maxBlockAscent, fm.ascent() / scaleFactor );
185
186 maxBlockCapHeight = std::max( maxBlockCapHeight, fm.capHeight() / scaleFactor );
187
188 blockHeightUsingAscentAccountingForVerticalOffset = std::max( std::max( maxBlockAscent, fragmentHeightForVerticallyOffsetText ), blockHeightUsingAscentAccountingForVerticalOffset );
189
190 maxBlockDescent = std::max( maxBlockDescent, fm.descent() / scaleFactor );
191 maxBlockMaxWidth = std::max( maxBlockMaxWidth, fm.maxWidth() / scaleFactor );
192
193 blockYMaxAdjustLabel = std::max( blockYMaxAdjustLabel, fragmentYMaxAdjust );
194
195 if ( ( fm.lineSpacing() / scaleFactor ) > maxLineSpacing )
196 {
197 maxLineSpacing = fm.lineSpacing() / scaleFactor;
198 maxBlockLeading = fm.leading() / scaleFactor;
199 }
200
201 fragmentFonts << updatedFont;
202
203 const double verticalOrientationFragmentHeight = fragmentIndex == 0 ? ( fm.ascent() / scaleFactor * fragment.text().size() + ( fragment.text().size() - 1 ) * updatedFont.letterSpacing() / scaleFactor )
204 : ( fragment.text().size() * ( fm.ascent() / scaleFactor + updatedFont.letterSpacing() / scaleFactor ) );
205 blockHeightVerticalOrientation += verticalOrientationFragmentHeight;
206 }
207
208 if ( blockIndex == 0 )
209 {
210 // same logic as used in QgsTextRenderer. (?!!)
211 // needed to move bottom of text's descender to within bottom edge of label
212 res.mFirstLineAscentOffset = 0.25 * maxBlockAscent; // descent() is not enough
213 res.mLastLineAscentOffset = res.mFirstLineAscentOffset;
214 res.mFirstLineCapHeight = maxBlockCapHeight;
215 const double lineHeight = ( maxBlockAscent + maxBlockDescent ); // ignore +1 for baseline
216
217 // rendering labels needs special handling - in this case text should be
218 // drawn with the bottom left corner coinciding with origin, vs top left
219 // for standard text rendering. Line height is also slightly different.
220 currentLabelBaseline = -res.mFirstLineAscentOffset;
221
222 if ( blockHeightUsingAscentAccountingForVerticalOffset > maxBlockAscent )
223 outerYMinLabel = maxBlockAscent - blockHeightUsingAscentAccountingForVerticalOffset;
224
225 // standard rendering - designed to exactly replicate QPainter's drawText method
226 currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/;
227
228 currentCapHeightBasedBaseline = res.mFirstLineCapHeight;
229 currentAscentBasedBaseline = maxBlockAscent;
230
231 // standard rendering - designed to exactly replicate QPainter's drawText rect method
232 currentPointBaseline = 0;
233
234 heightLabelMode += blockHeightUsingAscentDescent;
235 heightPointRectMode += blockHeightUsingAscentDescent;
236 heightCapHeightMode += maxBlockCapHeight;
237 heightAscentMode += maxBlockAscent;
238 }
239 else
240 {
241 const double thisLineHeightUsingAscentDescent = format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( format.lineHeight() * ( maxBlockAscent + maxBlockDescent ) ) : lineHeightPainterUnits;
242 const double thisLineHeightUsingLineSpacing = format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( format.lineHeight() * maxLineSpacing ) : lineHeightPainterUnits;
243
244 currentLabelBaseline += thisLineHeightUsingAscentDescent;
245 currentRectBaseline += thisLineHeightUsingLineSpacing;
246 currentPointBaseline += thisLineHeightUsingLineSpacing;
247 // using cap height??
248 currentCapHeightBasedBaseline += thisLineHeightUsingLineSpacing;
249 // using ascent?
250 currentAscentBasedBaseline += thisLineHeightUsingLineSpacing;
251
252 heightLabelMode += thisLineHeightUsingAscentDescent;
253 heightPointRectMode += thisLineHeightUsingLineSpacing;
254 heightCapHeightMode += thisLineHeightUsingLineSpacing;
255 heightAscentMode += thisLineHeightUsingLineSpacing;
256 if ( blockIndex == blockSize - 1 )
257 res.mLastLineAscentOffset = 0.25 * maxBlockAscent;
258 }
259
260 if ( blockIndex == blockSize - 1 )
261 {
262 if ( blockYMaxAdjustLabel > maxBlockDescent )
263 outerYMaxLabel = blockYMaxAdjustLabel - maxBlockDescent;
264 }
265
266 blockVerticalLineSpacing << ( format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( maxBlockMaxWidth * format.lineHeight() ) : lineHeightPainterUnits );
267
268 res.mBlockHeights << blockHeightUsingLineSpacing;
269
270 width = std::max( width, blockWidth );
271 outerXMax = std::max( outerXMax, blockXMax );
272
273 heightVerticalOrientation = std::max( heightVerticalOrientation, blockHeightVerticalOrientation );
274 res.mBlockWidths << blockWidth;
275 res.mFragmentFonts << fragmentFonts;
276 res.mBaselineOffsetsLabelMode << currentLabelBaseline;
277 res.mBaselineOffsetsPointMode << currentPointBaseline;
278 res.mBaselineOffsetsRectMode << currentRectBaseline;
279 res.mBaselineOffsetsCapHeightMode << currentCapHeightBasedBaseline;
280 res.mBaselineOffsetsAscentBased << currentAscentBasedBaseline;
281 res.mBlockMaxDescent << maxBlockDescent;
282 res.mBlockMaxCharacterWidth << maxBlockMaxWidth;
283 res.mFragmentVerticalOffsetsLabelMode << fragmentVerticalOffsets;
284 res.mFragmentVerticalOffsetsRectMode << fragmentVerticalOffsets;
285 res.mFragmentVerticalOffsetsPointMode << fragmentVerticalOffsets;
286 res.mFragmentHorizontalAdvance << fragmentHorizontalAdvance;
287
288 if ( blockIndex > 0 )
289 lastLineLeading = maxBlockLeading;
290 }
291
292 heightLabelMode -= lastLineLeading;
293 heightPointRectMode -= lastLineLeading;
294
295 res.mDocumentSizeLabelMode = QSizeF( width, heightLabelMode );
296 res.mDocumentSizePointRectMode = QSizeF( width, heightPointRectMode );
297 res.mDocumentSizeCapHeightMode = QSizeF( width, heightCapHeightMode );
298 res.mDocumentSizeAscentMode = QSizeF( width, heightAscentMode );
299
300 // adjust baselines
301 if ( !res.mBaselineOffsetsLabelMode.isEmpty() )
302 {
303 const double labelModeBaselineAdjust = res.mBaselineOffsetsLabelMode.constLast() + res.mLastLineAscentOffset;
304 const double pointModeBaselineAdjust = res.mBaselineOffsetsPointMode.constLast();
305 for ( int i = 0; i < blockSize; ++i )
306 {
307 res.mBaselineOffsetsLabelMode[i] -= labelModeBaselineAdjust;
308 res.mBaselineOffsetsPointMode[i] -= pointModeBaselineAdjust;
309 }
310 }
311
312 if ( !res.mBlockMaxCharacterWidth.isEmpty() )
313 {
314 QList< double > adjustedRightToLeftXOffsets;
315 double currentOffset = 0;
316 const int size = res.mBlockMaxCharacterWidth.size();
317
318 double widthVerticalOrientation = 0;
319 for ( int i = 0; i < size; ++i )
320 {
321 const double rightToLeftBlockMaxCharacterWidth = res.mBlockMaxCharacterWidth[size - 1 - i ];
322 const double rightToLeftLineSpacing = blockVerticalLineSpacing[ size - 1 - i ];
323
324 adjustedRightToLeftXOffsets << currentOffset;
325 currentOffset += rightToLeftLineSpacing;
326
327 if ( i == size - 1 )
328 widthVerticalOrientation += rightToLeftBlockMaxCharacterWidth;
329 else
330 widthVerticalOrientation += rightToLeftLineSpacing;
331 }
332 std::reverse( adjustedRightToLeftXOffsets.begin(), adjustedRightToLeftXOffsets.end() );
333 res.mVerticalOrientationXOffsets = adjustedRightToLeftXOffsets;
334
335 res.mDocumentSizeVerticalOrientation = QSizeF( widthVerticalOrientation, heightVerticalOrientation );
336 }
337
338 res.mOuterBoundsLabelMode = QRectF( outerXMin, -outerYMaxLabel,
339 outerXMax - outerXMin,
340 heightLabelMode - outerYMinLabel + outerYMaxLabel );
341
342 return res;
343}
344
346{
347 switch ( orientation )
348 {
350 switch ( mode )
351 {
354 return mDocumentSizePointRectMode;
355
357 return mDocumentSizeCapHeightMode;
358
360 return mDocumentSizeAscentMode;
361
363 return mDocumentSizeLabelMode;
364 };
366
368 return mDocumentSizeVerticalOrientation;
370 return QSizeF(); // label mode only
371 }
372
374}
375
377{
378 switch ( orientation )
379 {
381 switch ( mode )
382 {
387 return QRectF();
388
390 return mOuterBoundsLabelMode;
391 };
393
396 return QRectF(); // label mode only
397 }
398
400}
401
402double QgsTextDocumentMetrics::blockWidth( int blockIndex ) const
403{
404 return mBlockWidths.value( blockIndex );
405}
406
407double QgsTextDocumentMetrics::blockHeight( int blockIndex ) const
408{
409 return mBlockHeights.value( blockIndex );
410}
411
413{
414 return mFirstLineCapHeight;
415}
416
418{
419 switch ( mode )
420 {
422 return mBaselineOffsetsRectMode.value( blockIndex );
424 return mBaselineOffsetsCapHeightMode.value( blockIndex );
426 return mBaselineOffsetsAscentBased.value( blockIndex );
428 return mBaselineOffsetsPointMode.value( blockIndex );
430 return mBaselineOffsetsLabelMode.value( blockIndex );
431 }
433}
434
435double QgsTextDocumentMetrics::fragmentHorizontalAdvance( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
436{
437 return mFragmentHorizontalAdvance.value( blockIndex ).value( fragmentIndex );
438}
439
440double QgsTextDocumentMetrics::fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const
441{
442 switch ( mode )
443 {
447 return mFragmentVerticalOffsetsRectMode.value( blockIndex ).value( fragmentIndex );
449 return mFragmentVerticalOffsetsPointMode.value( blockIndex ).value( fragmentIndex );
451 return mFragmentVerticalOffsetsLabelMode.value( blockIndex ).value( fragmentIndex );
452 }
454}
455
457{
458 return mVerticalOrientationXOffsets.value( blockIndex );
459}
460
462{
463 return mBlockMaxCharacterWidth.value( blockIndex );
464}
465
467{
468 return mBlockMaxDescent.value( blockIndex );
469}
470
471QFont QgsTextDocumentMetrics::fragmentFont( int blockIndex, int fragmentIndex ) const
472{
473 return mFragmentFonts.value( blockIndex ).value( fragmentIndex );
474}
475
TextLayoutMode
Text layout modes.
Definition: qgis.h:2383
@ Labeling
Labeling-specific layout mode.
@ Point
Text at point of origin layout mode.
@ RectangleAscentBased
Similar to Rectangle mode, but uses ascents only when calculating font and line heights....
@ RectangleCapHeightBased
Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line ...
@ Rectangle
Text within rectangle layout mode.
TextOrientation
Text orientations.
Definition: qgis.h:2368
@ Vertical
Vertically oriented text.
@ RotationBased
Horizontally or vertically oriented text based on rotation (only available for map labeling)
@ Horizontal
Horizontally oriented text.
@ Normal
Adjacent characters are positioned in the standard way for text in the writing system in use.
@ SubScript
Characters are placed below the base line for normal text.
@ SuperScript
Characters are placed above the base line for normal text.
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size)
Contains information about the context of a rendering operation.
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).
Represents a block of text consisting of one or more QgsTextFragment objects.
Definition: qgstextblock.h:36
int size() const
Returns the number of fragments in the block.
const QgsTextFragment & at(int index) const
Returns the fragment at the specified index.
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 ...
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.
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.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
double firstLineCapHeight() const
Returns the cap height for the first line of text.
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.
QRectF outerBounds(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the outer bounds of the document, which is the documentSize() adjusted to account for any tex...
static QgsTextDocumentMetrics calculateMetrics(const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor=1.0)
Returns precalculated text metrics for a text document, when rendered using the given base format and...
double blockHeight(int blockIndex) const
Returns the height of the block at the specified index.
double 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.
double blockWidth(int blockIndex) const
Returns the width of the block at the specified index.
Represents a document consisting of one or more QgsTextBlock objects.
const QgsTextBlock & at(int index) const
Returns the block at the specified index.
int size() const
Returns the number of blocks in the document.
Container for all settings relating to text rendering.
Definition: qgstextformat.h:41
double lineHeight() const
Returns the line height for text.
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...
Qgis::RenderUnit lineHeightUnit() const
Returns the units for the line height for text.
Stores a fragment of text along with formatting overrides to be used when rendering the fragment.
QString text() const
Returns the text content of the fragment.
const QgsTextCharacterFormat & characterFormat() const
Returns the character formatting for the fragment.
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.
#define BUILTIN_UNREACHABLE
Definition: qgis.h:5853
constexpr double SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR
constexpr double SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR