QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
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
24#include <QFontMetricsF>
25
26// to match QTextEngine handling of superscript/subscript font sizes
28
29// to match Qt behavior in QTextLine::draw
32
33QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor )
34{
36
37 const QFont font = format.scaledFont( context, scaleFactor, &res.mIsNullSize );
38 if ( res.isNullFontSize() )
39 return res;
40
41 // for absolute line heights
42 const double lineHeightPainterUnits = context.convertToPainterUnits( format.lineHeight(), format.lineHeightUnit() );
43
44 double width = 0;
45 double heightLabelMode = 0;
46 double heightPointRectMode = 0;
47 double heightCapHeightMode = 0;
48 double heightAscentMode = 0;
49 const int blockSize = document.size();
50 res.mFragmentFonts.reserve( blockSize );
51 double currentLabelBaseline = 0;
52 double currentPointBaseline = 0;
53 double currentRectBaseline = 0;
54 double currentCapHeightBasedBaseline = 0;
55 double currentAscentBasedBaseline = 0;
56 double lastLineLeading = 0;
57
58 double heightVerticalOrientation = 0;
59
60 QVector < double > blockVerticalLineSpacing;
61
62 double outerXMin = 0;
63 double outerXMax = 0;
64 double outerYMinLabel = 0;
65 double outerYMaxLabel = 0;
66
67 for ( int blockIndex = 0; blockIndex < blockSize; blockIndex++ )
68 {
69 const QgsTextBlock &block = document.at( blockIndex );
70
71 double blockWidth = 0;
72 double blockXMax = 0;
73 double blockYMaxAdjustLabel = 0;
74
75 double blockHeightUsingAscentDescent = 0;
76 double blockHeightUsingLineSpacing = 0;
77 double blockHeightVerticalOrientation = 0;
78
79 double blockHeightUsingAscentAccountingForVerticalOffset = 0;
80
81 const int fragmentSize = block.size();
82
83 double maxBlockAscent = 0;
84 double maxBlockDescent = 0;
85 double maxLineSpacing = 0;
86 double maxBlockLeading = 0;
87 double maxBlockMaxWidth = 0;
88 double maxBlockCapHeight = 0;
89
90 QList< double > fragmentVerticalOffsets;
91 fragmentVerticalOffsets.reserve( fragmentSize );
92
93 QList< QFont > fragmentFonts;
94 fragmentFonts.reserve( fragmentSize );
95 QList< double >fragmentHorizontalAdvance;
96 fragmentHorizontalAdvance.reserve( fragmentSize );
97
98 QFont previousNonSuperSubScriptFont;
99
100 for ( int fragmentIndex = 0; fragmentIndex < fragmentSize; ++fragmentIndex )
101 {
102 const QgsTextFragment &fragment = block.at( fragmentIndex );
103 const QgsTextCharacterFormat &fragmentFormat = fragment.characterFormat();
104
105 double fragmentHeightForVerticallyOffsetText = 0;
106 double fragmentYMaxAdjust = 0;
107
108 QFont updatedFont = font;
109 fragmentFormat.updateFontForFormat( updatedFont, context, scaleFactor );
110
111 QFontMetricsF fm( updatedFont );
112
113 if ( fragmentIndex == 0 )
114 previousNonSuperSubScriptFont = updatedFont;
115
116 double fragmentVerticalOffset = 0;
117 if ( fragmentFormat.hasVerticalAlignmentSet() )
118 {
119 switch ( fragmentFormat.verticalAlignment() )
120 {
122 previousNonSuperSubScriptFont = updatedFont;
123 break;
124
126 {
127 const QFontMetricsF previousFM( previousNonSuperSubScriptFont );
128
129 if ( fragmentFormat.fontPointSize() < 0 )
130 {
131 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
132 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
133 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
134 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
135 // which we should respect
136 updatedFont.setPixelSize( static_cast< int >( std::round( updatedFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
137 fm = QFontMetricsF( updatedFont );
138 }
139
140 // to match Qt behavior in QTextLine::draw
141 fragmentVerticalOffset = -( previousFM.ascent() + previousFM.descent() ) * SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
142
143 // note -- this should really be fm.ascent(), not fm.capHeight() -- but in practice the ascent of most fonts is too large
144 // and causes unnecessarily large bounding boxes of vertically offset text!
145 fragmentHeightForVerticallyOffsetText = -fragmentVerticalOffset + fm.capHeight() / scaleFactor;
146 break;
147 }
148
150 {
151 const QFontMetricsF previousFM( previousNonSuperSubScriptFont );
152
153 if ( fragmentFormat.fontPointSize() < 0 )
154 {
155 // see above!!
156 updatedFont.setPixelSize( static_cast< int>( std::round( updatedFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
157 fm = QFontMetricsF( updatedFont );
158 }
159
160 // to match Qt behavior in QTextLine::draw
161 fragmentVerticalOffset = ( previousFM.ascent() + previousFM.descent() ) * SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
162
163 fragmentYMaxAdjust = fragmentVerticalOffset + fm.descent() / scaleFactor;
164 break;
165 }
166 }
167 }
168 else
169 {
170 previousNonSuperSubScriptFont = updatedFont;
171 }
172 fragmentVerticalOffsets << fragmentVerticalOffset;
173
174 const double fragmentWidth = fm.horizontalAdvance( fragment.text() ) / scaleFactor;
175
176 fragmentHorizontalAdvance << fragmentWidth;
177
178 const double fragmentHeightUsingAscentDescent = ( fm.ascent() + fm.descent() ) / scaleFactor;
179 const double fragmentHeightUsingLineSpacing = fm.lineSpacing() / scaleFactor;
180
181 blockWidth += fragmentWidth;
182 blockXMax += fragmentWidth;
183 blockHeightUsingAscentDescent = std::max( blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent );
184
185 blockHeightUsingLineSpacing = std::max( blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
186 maxBlockAscent = std::max( maxBlockAscent, fm.ascent() / scaleFactor );
187
188 maxBlockCapHeight = std::max( maxBlockCapHeight, fm.capHeight() / scaleFactor );
189
190 blockHeightUsingAscentAccountingForVerticalOffset = std::max( std::max( maxBlockAscent, fragmentHeightForVerticallyOffsetText ), blockHeightUsingAscentAccountingForVerticalOffset );
191
192 maxBlockDescent = std::max( maxBlockDescent, fm.descent() / scaleFactor );
193 maxBlockMaxWidth = std::max( maxBlockMaxWidth, fm.maxWidth() / scaleFactor );
194
195 blockYMaxAdjustLabel = std::max( blockYMaxAdjustLabel, fragmentYMaxAdjust );
196
197 if ( ( fm.lineSpacing() / scaleFactor ) > maxLineSpacing )
198 {
199 maxLineSpacing = fm.lineSpacing() / scaleFactor;
200 maxBlockLeading = fm.leading() / scaleFactor;
201 }
202
203 fragmentFonts << updatedFont;
204
205 const double verticalOrientationFragmentHeight = fragmentIndex == 0 ? ( fm.ascent() / scaleFactor * fragment.text().size() + ( fragment.text().size() - 1 ) * updatedFont.letterSpacing() / scaleFactor )
206 : ( fragment.text().size() * ( fm.ascent() / scaleFactor + updatedFont.letterSpacing() / scaleFactor ) );
207 blockHeightVerticalOrientation += verticalOrientationFragmentHeight;
208 }
209
210 if ( blockIndex == 0 )
211 {
212 // same logic as used in QgsTextRenderer. (?!!)
213 // needed to move bottom of text's descender to within bottom edge of label
214 res.mFirstLineAscentOffset = 0.25 * maxBlockAscent; // descent() is not enough
215 res.mLastLineAscentOffset = res.mFirstLineAscentOffset;
216 res.mFirstLineCapHeight = maxBlockCapHeight;
217 const double lineHeight = ( maxBlockAscent + maxBlockDescent ); // ignore +1 for baseline
218
219 // rendering labels needs special handling - in this case text should be
220 // drawn with the bottom left corner coinciding with origin, vs top left
221 // for standard text rendering. Line height is also slightly different.
222 currentLabelBaseline = -res.mFirstLineAscentOffset;
223
224 if ( blockHeightUsingAscentAccountingForVerticalOffset > maxBlockAscent )
225 outerYMinLabel = maxBlockAscent - blockHeightUsingAscentAccountingForVerticalOffset;
226
227 // standard rendering - designed to exactly replicate QPainter's drawText method
228 currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/;
229
230 currentCapHeightBasedBaseline = res.mFirstLineCapHeight;
231 currentAscentBasedBaseline = maxBlockAscent;
232
233 // standard rendering - designed to exactly replicate QPainter's drawText rect method
234 currentPointBaseline = 0;
235
236 heightLabelMode += blockHeightUsingAscentDescent;
237 heightPointRectMode += blockHeightUsingAscentDescent;
238 heightCapHeightMode += maxBlockCapHeight;
239 heightAscentMode += maxBlockAscent;
240 }
241 else
242 {
243 const double thisLineHeightUsingAscentDescent = format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( format.lineHeight() * ( maxBlockAscent + maxBlockDescent ) ) : lineHeightPainterUnits;
244 const double thisLineHeightUsingLineSpacing = format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( format.lineHeight() * maxLineSpacing ) : lineHeightPainterUnits;
245
246 currentLabelBaseline += thisLineHeightUsingAscentDescent;
247 currentRectBaseline += thisLineHeightUsingLineSpacing;
248 currentPointBaseline += thisLineHeightUsingLineSpacing;
249 // using cap height??
250 currentCapHeightBasedBaseline += thisLineHeightUsingLineSpacing;
251 // using ascent?
252 currentAscentBasedBaseline += thisLineHeightUsingLineSpacing;
253
254 heightLabelMode += thisLineHeightUsingAscentDescent;
255 heightPointRectMode += thisLineHeightUsingLineSpacing;
256 heightCapHeightMode += thisLineHeightUsingLineSpacing;
257 heightAscentMode += thisLineHeightUsingLineSpacing;
258 if ( blockIndex == blockSize - 1 )
259 res.mLastLineAscentOffset = 0.25 * maxBlockAscent;
260 }
261
262 if ( blockIndex == blockSize - 1 )
263 {
264 if ( blockYMaxAdjustLabel > maxBlockDescent )
265 outerYMaxLabel = blockYMaxAdjustLabel - maxBlockDescent;
266 }
267
268 blockVerticalLineSpacing << ( format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( maxBlockMaxWidth * format.lineHeight() ) : lineHeightPainterUnits );
269
270 res.mBlockHeights << blockHeightUsingLineSpacing;
271
272 width = std::max( width, blockWidth );
273 outerXMax = std::max( outerXMax, blockXMax );
274
275 heightVerticalOrientation = std::max( heightVerticalOrientation, blockHeightVerticalOrientation );
276 res.mBlockWidths << blockWidth;
277 res.mFragmentFonts << fragmentFonts;
278 res.mBaselineOffsetsLabelMode << currentLabelBaseline;
279 res.mBaselineOffsetsPointMode << currentPointBaseline;
280 res.mBaselineOffsetsRectMode << currentRectBaseline;
281 res.mBaselineOffsetsCapHeightMode << currentCapHeightBasedBaseline;
282 res.mBaselineOffsetsAscentBased << currentAscentBasedBaseline;
283 res.mBlockMaxDescent << maxBlockDescent;
284 res.mBlockMaxCharacterWidth << maxBlockMaxWidth;
285 res.mFragmentVerticalOffsetsLabelMode << fragmentVerticalOffsets;
286 res.mFragmentVerticalOffsetsRectMode << fragmentVerticalOffsets;
287 res.mFragmentVerticalOffsetsPointMode << fragmentVerticalOffsets;
288 res.mFragmentHorizontalAdvance << fragmentHorizontalAdvance;
289
290 if ( blockIndex > 0 )
291 lastLineLeading = maxBlockLeading;
292 }
293
294 heightLabelMode -= lastLineLeading;
295 heightPointRectMode -= lastLineLeading;
296
297 res.mDocumentSizeLabelMode = QSizeF( width, heightLabelMode );
298 res.mDocumentSizePointRectMode = QSizeF( width, heightPointRectMode );
299 res.mDocumentSizeCapHeightMode = QSizeF( width, heightCapHeightMode );
300 res.mDocumentSizeAscentMode = QSizeF( width, heightAscentMode );
301
302 // adjust baselines
303 if ( !res.mBaselineOffsetsLabelMode.isEmpty() )
304 {
305 const double labelModeBaselineAdjust = res.mBaselineOffsetsLabelMode.constLast() + res.mLastLineAscentOffset;
306 const double pointModeBaselineAdjust = res.mBaselineOffsetsPointMode.constLast();
307 for ( int i = 0; i < blockSize; ++i )
308 {
309 res.mBaselineOffsetsLabelMode[i] -= labelModeBaselineAdjust;
310 res.mBaselineOffsetsPointMode[i] -= pointModeBaselineAdjust;
311 }
312 }
313
314 if ( !res.mBlockMaxCharacterWidth.isEmpty() )
315 {
316 QList< double > adjustedRightToLeftXOffsets;
317 double currentOffset = 0;
318 const int size = res.mBlockMaxCharacterWidth.size();
319
320 double widthVerticalOrientation = 0;
321 for ( int i = 0; i < size; ++i )
322 {
323 const double rightToLeftBlockMaxCharacterWidth = res.mBlockMaxCharacterWidth[size - 1 - i ];
324 const double rightToLeftLineSpacing = blockVerticalLineSpacing[ size - 1 - i ];
325
326 adjustedRightToLeftXOffsets << currentOffset;
327 currentOffset += rightToLeftLineSpacing;
328
329 if ( i == size - 1 )
330 widthVerticalOrientation += rightToLeftBlockMaxCharacterWidth;
331 else
332 widthVerticalOrientation += rightToLeftLineSpacing;
333 }
334 std::reverse( adjustedRightToLeftXOffsets.begin(), adjustedRightToLeftXOffsets.end() );
335 res.mVerticalOrientationXOffsets = adjustedRightToLeftXOffsets;
336
337 res.mDocumentSizeVerticalOrientation = QSizeF( widthVerticalOrientation, heightVerticalOrientation );
338 }
339
340 res.mOuterBoundsLabelMode = QRectF( outerXMin, -outerYMaxLabel,
341 outerXMax - outerXMin,
342 heightLabelMode - outerYMinLabel + outerYMaxLabel );
343
344 return res;
345}
346
348{
349 switch ( orientation )
350 {
351 case Qgis::TextOrientation::Horizontal:
352 switch ( mode )
353 {
354 case Qgis::TextLayoutMode::Rectangle:
356 return mDocumentSizePointRectMode;
357
359 return mDocumentSizeCapHeightMode;
360
362 return mDocumentSizeAscentMode;
363
364 case Qgis::TextLayoutMode::Labeling:
365 return mDocumentSizeLabelMode;
366 };
368
369 case Qgis::TextOrientation::Vertical:
370 return mDocumentSizeVerticalOrientation;
371 case Qgis::TextOrientation::RotationBased:
372 return QSizeF(); // label mode only
373 }
374
376}
377
379{
380 switch ( orientation )
381 {
382 case Qgis::TextOrientation::Horizontal:
383 switch ( mode )
384 {
385 case Qgis::TextLayoutMode::Rectangle:
389 return QRectF();
390
391 case Qgis::TextLayoutMode::Labeling:
392 return mOuterBoundsLabelMode;
393 };
395
396 case Qgis::TextOrientation::Vertical:
397 case Qgis::TextOrientation::RotationBased:
398 return QRectF(); // label mode only
399 }
400
402}
403
404double QgsTextDocumentMetrics::blockWidth( int blockIndex ) const
405{
406 return mBlockWidths.value( blockIndex );
407}
408
409double QgsTextDocumentMetrics::blockHeight( int blockIndex ) const
410{
411 return mBlockHeights.value( blockIndex );
412}
413
415{
416 return mFirstLineCapHeight;
417}
418
420{
421 switch ( mode )
422 {
423 case Qgis::TextLayoutMode::Rectangle:
424 return mBaselineOffsetsRectMode.value( blockIndex );
426 return mBaselineOffsetsCapHeightMode.value( blockIndex );
428 return mBaselineOffsetsAscentBased.value( blockIndex );
430 return mBaselineOffsetsPointMode.value( blockIndex );
431 case Qgis::TextLayoutMode::Labeling:
432 return mBaselineOffsetsLabelMode.value( blockIndex );
433 }
435}
436
437double QgsTextDocumentMetrics::fragmentHorizontalAdvance( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
438{
439 return mFragmentHorizontalAdvance.value( blockIndex ).value( fragmentIndex );
440}
441
442double QgsTextDocumentMetrics::fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const
443{
444 switch ( mode )
445 {
446 case Qgis::TextLayoutMode::Rectangle:
449 return mFragmentVerticalOffsetsRectMode.value( blockIndex ).value( fragmentIndex );
451 return mFragmentVerticalOffsetsPointMode.value( blockIndex ).value( fragmentIndex );
452 case Qgis::TextLayoutMode::Labeling:
453 return mFragmentVerticalOffsetsLabelMode.value( blockIndex ).value( fragmentIndex );
454 }
456}
457
459{
460 return mVerticalOrientationXOffsets.value( blockIndex );
461}
462
464{
465 return mBlockMaxCharacterWidth.value( blockIndex );
466}
467
469{
470 return mBlockMaxDescent.value( blockIndex );
471}
472
473QFont QgsTextDocumentMetrics::fragmentFont( int blockIndex, int fragmentIndex ) const
474{
475 return mFragmentFonts.value( blockIndex ).value( fragmentIndex );
476}
477
TextLayoutMode
Text layout modes.
Definition: qgis.h:1801
@ 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 ...
TextOrientation
Text orientations.
Definition: qgis.h:1786
@ 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.
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.
#define BUILTIN_UNREACHABLE
Definition: qgis.h:4180
constexpr double SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR
constexpr double SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR
constexpr double SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR