QGIS API Documentation 3.40.0-Bratislava (b56115d8743)
Loading...
Searching...
No Matches
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#include "qgsapplication.h"
25#include "qgsimagecache.h"
26
27#include <QFontMetricsF>
28
29// to match Qt behavior in QTextLine::draw
32
34{
37 double width = 0;
38 double heightLabelMode = 0;
41 double heightAscentMode = 0;
42 int blockSize = 0;
48 double lastLineLeading = 0;
49
51
52 QVector < double > blockVerticalLineSpacing;
53
54 double outerXMin = 0;
55 double outerXMax = 0;
56 double outerYMinLabel = 0;
57 double outerYMaxLabel = 0;
58};
59
87
88
89void QgsTextDocumentMetrics::finalizeBlock( QgsTextDocumentMetrics &res, const QgsTextFormat &format, DocumentMetrics &documentMetrics, QgsTextBlock &outputBlock, BlockMetrics &metrics )
90{
91 if ( metrics.isFirstBlock )
92 {
93 // same logic as used in QgsTextRenderer. (?!!)
94 // needed to move bottom of text's descender to within bottom edge of label
95 res.mFirstLineAscentOffset = 0.25 * metrics.maxBlockAscentForTextFragments; // descent() is not enough
96 res.mLastLineAscentOffset = res.mFirstLineAscentOffset;
97 res.mFirstLineCapHeight = metrics.maxBlockCapHeight;
98 const double lineHeight = ( metrics.maxBlockAscent + metrics.maxBlockDescent ); // ignore +1 for baseline
99
100 // rendering labels needs special handling - in this case text should be
101 // drawn with the bottom left corner coinciding with origin, vs top left
102 // for standard text rendering. Line height is also slightly different.
103 documentMetrics.currentLabelBaseline = -res.mFirstLineAscentOffset;
104
107
108 // standard rendering - designed to exactly replicate QPainter's drawText method
109 documentMetrics.currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/;
110
111 documentMetrics.currentCapHeightBasedBaseline = res.mFirstLineCapHeight;
112 documentMetrics.currentAscentBasedBaseline = metrics.maxBlockAscent;
113
114 // standard rendering - designed to exactly replicate QPainter's drawText rect method
115 documentMetrics.currentPointBaseline = 0;
116
117 documentMetrics.heightLabelMode += metrics.blockHeightUsingAscentDescent;
118 documentMetrics.heightPointRectMode += metrics.blockHeightUsingAscentDescent;
119 documentMetrics.heightCapHeightMode += metrics.maxBlockCapHeight;
120 documentMetrics.heightAscentMode += metrics.maxBlockAscent;
121 }
122 else
123 {
124 double thisLineHeightUsingAscentDescent = format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( format.lineHeight() * ( metrics.maxBlockAscent + metrics.maxBlockDescent ) ) : documentMetrics.lineHeightPainterUnits;
125 double thisLineHeightUsingLineSpacing = format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( format.lineHeight() * metrics.maxLineSpacing ) : documentMetrics.lineHeightPainterUnits;
126
127 thisLineHeightUsingAscentDescent = std::max( thisLineHeightUsingAscentDescent, metrics.maxBlockFixedItemHeight );
128 thisLineHeightUsingLineSpacing = std::max( thisLineHeightUsingLineSpacing, metrics.maxBlockFixedItemHeight );
129
130 documentMetrics.currentLabelBaseline += thisLineHeightUsingAscentDescent;
131 documentMetrics.currentRectBaseline += thisLineHeightUsingLineSpacing;
132 documentMetrics.currentPointBaseline += thisLineHeightUsingLineSpacing;
133 // using cap height??
134 documentMetrics.currentCapHeightBasedBaseline += thisLineHeightUsingLineSpacing;
135 // using ascent?
136 documentMetrics.currentAscentBasedBaseline += thisLineHeightUsingLineSpacing;
137
138 documentMetrics.heightLabelMode += thisLineHeightUsingAscentDescent;
139 documentMetrics.heightPointRectMode += thisLineHeightUsingLineSpacing;
140 documentMetrics.heightCapHeightMode += thisLineHeightUsingLineSpacing;
141 documentMetrics.heightAscentMode += thisLineHeightUsingLineSpacing;
142 if ( metrics.isLastBlock )
143 res.mLastLineAscentOffset = 0.25 * metrics.maxBlockAscentForTextFragments;
144 }
145
146 if ( metrics.isLastBlock )
147 {
148 if ( metrics.blockYMaxAdjustLabel > metrics.maxBlockDescent )
149 documentMetrics.outerYMaxLabel = metrics.blockYMaxAdjustLabel - metrics.maxBlockDescent;
150 }
151
152 documentMetrics.blockVerticalLineSpacing << ( format.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( metrics.maxBlockMaxWidth * format.lineHeight() ) : documentMetrics.lineHeightPainterUnits );
153
154 res.mBlockHeights << metrics.blockHeightUsingLineSpacing;
155
156 documentMetrics.width = std::max( documentMetrics.width, metrics.blockWidth );
157 documentMetrics.outerXMax = std::max( documentMetrics.outerXMax, metrics.blockXMax );
158
159 documentMetrics.heightVerticalOrientation = std::max( documentMetrics.heightVerticalOrientation, metrics.blockHeightVerticalOrientation );
160 res.mBlockWidths << metrics.blockWidth;
161 res.mFragmentFonts << metrics.fragmentFonts;
162 res.mBaselineOffsetsLabelMode << documentMetrics.currentLabelBaseline;
163 res.mBaselineOffsetsPointMode << documentMetrics.currentPointBaseline;
164 res.mBaselineOffsetsRectMode << documentMetrics.currentRectBaseline;
165 res.mBaselineOffsetsCapHeightMode << documentMetrics.currentCapHeightBasedBaseline;
166 res.mBaselineOffsetsAscentBased << documentMetrics.currentAscentBasedBaseline;
167 res.mBlockMaxDescent << metrics.maxBlockDescent;
168 res.mBlockMaxCharacterWidth << metrics.maxBlockMaxWidth;
169 res.mFragmentVerticalOffsetsLabelMode << metrics.fragmentVerticalOffsets;
170 res.mFragmentFixedHeights << metrics.fragmentFixedHeights;
171 res.mFragmentVerticalOffsetsRectMode << metrics.fragmentVerticalOffsets;
172 res.mFragmentVerticalOffsetsPointMode << metrics.fragmentVerticalOffsets;
173 res.mFragmentHorizontalAdvance << metrics.fragmentHorizontalAdvance;
174
175 res.mDocument.append( outputBlock );
176 outputBlock.clear();
177
178 if ( !metrics.isFirstBlock )
179 documentMetrics.lastLineLeading = metrics.maxBlockLeading;
180
181 // reset metrics for next block
182 metrics = BlockMetrics();
183};
184
185
186void QgsTextDocumentMetrics::processFragment( QgsTextDocumentMetrics &res, const QgsTextFormat &format, const QgsRenderContext &context, const QgsTextDocumentRenderContext &documentContext, double scaleFactor, DocumentMetrics &documentMetrics, BlockMetrics &thisBlockMetrics, const QFont &font, const QgsTextFragment &fragment, QgsTextBlock &currentOutputBlock )
187{
188 if ( fragment.isTab() )
189 {
190 // special handling for tab characters
191 const double nextTabStop = ( std::floor( thisBlockMetrics.blockXMax / documentMetrics.tabStopDistancePainterUnits ) + 1 ) * documentMetrics.tabStopDistancePainterUnits;
192 const double fragmentWidth = nextTabStop - thisBlockMetrics.blockXMax;
193
194 thisBlockMetrics.blockWidth += fragmentWidth;
195 thisBlockMetrics.blockXMax += fragmentWidth;
196
197 thisBlockMetrics.fragmentVerticalOffsets << 0;
198 thisBlockMetrics.fragmentHorizontalAdvance << fragmentWidth;
199 thisBlockMetrics.fragmentFixedHeights << -1;
200 thisBlockMetrics.fragmentFonts << QFont();
201 currentOutputBlock.append( fragment );
202 }
203 else
204 {
205 const QgsTextCharacterFormat &fragmentFormat = fragment.characterFormat();
206
207 double fragmentHeightForVerticallyOffsetText = 0;
208 double fragmentYMaxAdjust = 0;
209
210 QFont updatedFont = font;
211 fragmentFormat.updateFontForFormat( updatedFont, context, scaleFactor );
212
213 QFontMetricsF fm( updatedFont );
214
215 // first, just do what we need to calculate the fragment width. We need this upfront to determine if we need to split this fragment up into a new block
216 // in order to respect text wrapping
217 if ( thisBlockMetrics.isFirstNonTabFragment )
218 thisBlockMetrics.previousNonSuperSubScriptFont = updatedFont;
219
220 double fragmentVerticalOffset = 0;
221 if ( fragmentFormat.hasVerticalAlignmentSet() )
222 {
223 switch ( fragmentFormat.verticalAlignment() )
224 {
226 thisBlockMetrics.previousNonSuperSubScriptFont = updatedFont;
227 break;
228
230 {
231 const QFontMetricsF previousFM( thisBlockMetrics.previousNonSuperSubScriptFont );
232
233 if ( fragmentFormat.fontPointSize() < 0 )
234 {
235 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
236 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
237 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
238 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
239 // which we should respect
240 updatedFont.setPixelSize( static_cast< int >( std::round( updatedFont.pixelSize() * QgsTextRenderer::SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
241 fm = QFontMetricsF( updatedFont );
242 }
243
244 // to match Qt behavior in QTextLine::draw
245 fragmentVerticalOffset = -( previousFM.ascent() + previousFM.descent() ) * SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
246
247 // note -- this should really be fm.ascent(), not fm.capHeight() -- but in practice the ascent of most fonts is too large
248 // and causes unnecessarily large bounding boxes of vertically offset text!
249 fragmentHeightForVerticallyOffsetText = -fragmentVerticalOffset + fm.capHeight() / scaleFactor;
250 break;
251 }
252
254 {
255 const QFontMetricsF previousFM( thisBlockMetrics.previousNonSuperSubScriptFont );
256
257 if ( fragmentFormat.fontPointSize() < 0 )
258 {
259 // see above!!
260 updatedFont.setPixelSize( static_cast< int>( std::round( updatedFont.pixelSize() * QgsTextRenderer::SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
261 fm = QFontMetricsF( updatedFont );
262 }
263
264 // to match Qt behavior in QTextLine::draw
265 fragmentVerticalOffset = ( previousFM.ascent() + previousFM.descent() ) * SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
266
267 fragmentYMaxAdjust = fragmentVerticalOffset + fm.descent() / scaleFactor;
268 break;
269 }
270 }
271 }
272 else
273 {
274 thisBlockMetrics.previousNonSuperSubScriptFont = updatedFont;
275 }
276
277 auto updateCommonBlockMetrics = [ &fragmentVerticalOffset,
278 &fragmentYMaxAdjust,
279 &fragmentHeightForVerticallyOffsetText,
280 &updatedFont,
281 &fm,
282 scaleFactor]( BlockMetrics & thisBlockMetrics, double fragmentWidth, const QgsTextFragment & fragment )
283 {
284 thisBlockMetrics.fragmentVerticalOffsets << fragmentVerticalOffset;
285 thisBlockMetrics.blockYMaxAdjustLabel = std::max( thisBlockMetrics.blockYMaxAdjustLabel, fragmentYMaxAdjust );
286 thisBlockMetrics.blockHeightUsingAscentAccountingForVerticalOffset = std::max( std::max( thisBlockMetrics.maxBlockAscent, fragmentHeightForVerticallyOffsetText ), thisBlockMetrics.blockHeightUsingAscentAccountingForVerticalOffset );
287
288 thisBlockMetrics.fragmentHorizontalAdvance << fragmentWidth;
289
290 thisBlockMetrics.blockWidth += fragmentWidth;
291 thisBlockMetrics.blockXMax += fragmentWidth;
292
293 thisBlockMetrics.fragmentFonts << updatedFont;
294
295 const double verticalOrientationFragmentHeight = thisBlockMetrics.isFirstNonTabFragment ? ( fm.ascent() / scaleFactor * fragment.text().size() + ( fragment.text().size() - 1 ) * updatedFont.letterSpacing() / scaleFactor )
296 : ( fragment.text().size() * ( fm.ascent() / scaleFactor + updatedFont.letterSpacing() / scaleFactor ) );
297 thisBlockMetrics.blockHeightVerticalOrientation += verticalOrientationFragmentHeight;
298
299 thisBlockMetrics.isFirstNonTabFragment = false;
300 };
301
302 // calculate width of fragment
303 if ( fragment.isImage() )
304 {
305 double imageHeight = 0;
306 double imageWidth = 0;
307 if ( ( qgsDoubleNear( fragmentFormat.imageSize().width(), 0 ) || fragmentFormat.imageSize().width() < 0 )
308 && ( qgsDoubleNear( fragmentFormat.imageSize().height(), 0 ) || fragmentFormat.imageSize().height() < 0 ) )
309 {
310 // use original image size
311 const QSize imageSize = QgsApplication::imageCache()->originalSize( fragmentFormat.imagePath(), context.flags() & Qgis::RenderContextFlag::RenderBlocking );
312 // TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
313 const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
314 const double pixelsPerMm = context.scaleFactor();
315 imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
316 imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
317 }
318 else if ( ( qgsDoubleNear( fragmentFormat.imageSize().width(), 0 ) || fragmentFormat.imageSize().width() < 0 ) )
319 {
320 // height specified, calculate width
321 const QSize originalImageSize = QgsApplication::imageCache()->originalSize( fragmentFormat.imagePath(), context.flags() & Qgis::RenderContextFlag::RenderBlocking );
322 imageHeight = context.convertToPainterUnits( fragmentFormat.imageSize().height(), Qgis::RenderUnit::Points );
323 imageWidth = originalImageSize.width() * imageHeight / originalImageSize.height();
324 }
325 else if ( ( qgsDoubleNear( fragmentFormat.imageSize().height(), 0 ) || fragmentFormat.imageSize().height() < 0 ) )
326 {
327 // width specified, calculate height
328 const QSize originalImageSize = QgsApplication::imageCache()->originalSize( fragmentFormat.imagePath(), context.flags() & Qgis::RenderContextFlag::RenderBlocking );
329 imageWidth = context.convertToPainterUnits( fragmentFormat.imageSize().width(), Qgis::RenderUnit::Points );
330 imageHeight = originalImageSize.height() * imageWidth / originalImageSize.width();
331 }
332 else
333 {
334 imageWidth = context.convertToPainterUnits( fragmentFormat.imageSize().width(), Qgis::RenderUnit::Points );
335 imageHeight = context.convertToPainterUnits( fragmentFormat.imageSize().height(), Qgis::RenderUnit::Points );
336 }
337
338 // do we need to move this image fragment to a new block to respect wrapping?
339 if ( documentContext.flags() & Qgis::TextRendererFlag::WrapLines && documentContext.maximumWidth() > 0
340 && ( thisBlockMetrics.blockXMax + imageWidth > documentContext.maximumWidth() )
341 && !currentOutputBlock.empty() )
342 {
343 // yep, need to wrap before the image
344 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
345 thisBlockMetrics.isFirstBlock = false;
346 }
347
348 // we consider the whole image as ascent, and descent as 0
349 thisBlockMetrics.blockHeightUsingAscentDescent = std::max( thisBlockMetrics.blockHeightUsingAscentDescent, imageHeight + fm.descent() / scaleFactor );
350 thisBlockMetrics.blockHeightUsingLineSpacing = std::max( thisBlockMetrics.blockHeightUsingLineSpacing, imageHeight + fm.leading() );
351
352 thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, imageHeight );
353 thisBlockMetrics.maxBlockCapHeight = std::max( thisBlockMetrics.maxBlockCapHeight, imageHeight );
354 thisBlockMetrics.maxLineSpacing = std::max( thisBlockMetrics.maxLineSpacing, imageHeight + fm.leading() / scaleFactor );
355 thisBlockMetrics.maxBlockLeading = std::max( thisBlockMetrics.maxBlockLeading, fm.leading() / scaleFactor );
356 thisBlockMetrics.maxBlockMaxWidth = std::max( thisBlockMetrics.maxBlockMaxWidth, imageWidth );
357 thisBlockMetrics.maxBlockFixedItemHeight = std::max( thisBlockMetrics.maxBlockFixedItemHeight, imageHeight );
358 thisBlockMetrics.fragmentFixedHeights << imageHeight;
359 updateCommonBlockMetrics( thisBlockMetrics, imageWidth, fragment );
360 currentOutputBlock.append( fragment );
361 }
362 else
363 {
364 const double fragmentHeightUsingAscentDescent = ( fm.ascent() + fm.descent() ) / scaleFactor;
365 const double fragmentHeightUsingLineSpacing = fm.lineSpacing() / scaleFactor;
366
367 auto finalizeTextFragment = [fragmentHeightUsingAscentDescent,
368 fragmentHeightUsingLineSpacing,
369 &fm,
370 scaleFactor,
371 &currentOutputBlock,
372 &updateCommonBlockMetrics
373 ]( BlockMetrics & thisBlockMetrics, const QgsTextFragment & fragment, double fragmentWidth )
374 {
375 thisBlockMetrics.blockHeightUsingAscentDescent = std::max( thisBlockMetrics.blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent );
376
377 thisBlockMetrics.blockHeightUsingLineSpacing = std::max( thisBlockMetrics.blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
378 thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, fm.ascent() / scaleFactor );
379 thisBlockMetrics.maxBlockAscentForTextFragments = std::max( thisBlockMetrics.maxBlockAscentForTextFragments, fm.ascent() / scaleFactor );
380
381 thisBlockMetrics.maxBlockCapHeight = std::max( thisBlockMetrics.maxBlockCapHeight, fm.capHeight() / scaleFactor );
382
383 thisBlockMetrics.maxBlockDescent = std::max( thisBlockMetrics.maxBlockDescent, fm.descent() / scaleFactor );
384 thisBlockMetrics.maxBlockMaxWidth = std::max( thisBlockMetrics.maxBlockMaxWidth, fm.maxWidth() / scaleFactor );
385
386 if ( ( fm.lineSpacing() / scaleFactor ) > thisBlockMetrics.maxLineSpacing )
387 {
388 thisBlockMetrics.maxLineSpacing = fm.lineSpacing() / scaleFactor;
389 thisBlockMetrics.maxBlockLeading = fm.leading() / scaleFactor;
390 }
391 thisBlockMetrics.fragmentFixedHeights << -1;
392 updateCommonBlockMetrics( thisBlockMetrics, fragmentWidth, fragment );
393 currentOutputBlock.append( fragment );
394 };
395
396 double fragmentWidth = fm.horizontalAdvance( fragment.text() ) / scaleFactor;
397
398 // do we need to split this fragment to respect wrapping?
399 if ( documentContext.flags() & Qgis::TextRendererFlag::WrapLines && documentContext.maximumWidth() > 0
400 && ( thisBlockMetrics.blockXMax + fragmentWidth > documentContext.maximumWidth() ) )
401 {
402 // yep, need to split the fragment!
403
404 //first step is to identify words which must be on their own line (too long to fit)
405 const QStringList words = fragment.text().split( ' ' );
406 QStringList linesToProcess;
407 QStringList wordsInCurrentLine;
408 double remainingWidthInCurrentLine = documentContext.maximumWidth() - thisBlockMetrics.blockXMax;
409 for ( const QString &word : words )
410 {
411 const double wordWidth = fm.horizontalAdvance( word ) / scaleFactor;
412 if ( wordWidth > remainingWidthInCurrentLine )
413 {
414 //too long to fit
415 if ( !wordsInCurrentLine.isEmpty() )
416 linesToProcess << wordsInCurrentLine.join( ' ' );
417 wordsInCurrentLine.clear();
418 linesToProcess << word;
419 remainingWidthInCurrentLine = documentContext.maximumWidth();
420 }
421 else
422 {
423 wordsInCurrentLine.append( word );
424 }
425 }
426 if ( !wordsInCurrentLine.isEmpty() )
427 linesToProcess << wordsInCurrentLine.join( ' ' );
428
429 remainingWidthInCurrentLine = documentContext.maximumWidth() - thisBlockMetrics.blockXMax;
430 for ( int lineIndex = 0; lineIndex < linesToProcess.size(); ++lineIndex )
431 {
432 QString remainingText = linesToProcess.at( lineIndex );
433 int lastPos = remainingText.lastIndexOf( ' ' );
434 while ( lastPos > -1 )
435 {
436 //check if remaining text is short enough to go in one line
437 if ( ( fm.horizontalAdvance( remainingText ) / scaleFactor ) <= remainingWidthInCurrentLine )
438 {
439 break;
440 }
441
442 const double widthTextToLastPos = fm.horizontalAdvance( remainingText.left( lastPos ) ) / scaleFactor;
443 if ( widthTextToLastPos <= remainingWidthInCurrentLine )
444 {
445 QgsTextFragment thisLineFragment;
446 thisLineFragment.setCharacterFormat( fragment.characterFormat() );
447 thisLineFragment.setText( remainingText.left( lastPos ) );
448 finalizeTextFragment( thisBlockMetrics, thisLineFragment, widthTextToLastPos );
449 // move to new block
450 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
451 thisBlockMetrics.isFirstBlock = false;
452 remainingWidthInCurrentLine = documentContext.maximumWidth();
453 remainingText = remainingText.mid( lastPos + 1 );
454 lastPos = 0;
455 }
456 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
457 }
458
459 // if too big, and block is not empty, then flush current block first
460 if ( ( fm.horizontalAdvance( remainingText ) / scaleFactor ) > remainingWidthInCurrentLine && !currentOutputBlock.empty() )
461 {
462 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
463 thisBlockMetrics.isFirstBlock = false;
464 remainingWidthInCurrentLine = documentContext.maximumWidth();
465 }
466
467 QgsTextFragment thisLineFragment;
468 thisLineFragment.setCharacterFormat( fragment.characterFormat() );
469 thisLineFragment.setText( remainingText );
470 finalizeTextFragment( thisBlockMetrics, thisLineFragment, fm.horizontalAdvance( remainingText ) / scaleFactor );
471
472 if ( lineIndex < linesToProcess.size() - 1 )
473 {
474 // start new block if we aren't at the last line
475 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
476 thisBlockMetrics.isFirstBlock = false;
477 remainingWidthInCurrentLine = documentContext.maximumWidth();
478 }
479
480 thisBlockMetrics.isFirstBlock = false;
481 }
482 }
483 else
484 {
485 // simple case, no wrapping
486 finalizeTextFragment( thisBlockMetrics, fragment, fragmentWidth );
487 }
488 }
489 }
490}
491
492QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor, const QgsTextDocumentRenderContext &documentContext )
493{
495
496 const QFont font = format.scaledFont( context, scaleFactor, &res.mIsNullSize );
497 if ( res.isNullFontSize() )
498 return res;
499
500 DocumentMetrics documentMetrics;
501
502 // for absolute line heights
503 documentMetrics.lineHeightPainterUnits = context.convertToPainterUnits( format.lineHeight(), format.lineHeightUnit() );
504
506 ? format.tabStopDistance() * font.pixelSize() / scaleFactor
508
509 documentMetrics.blockSize = document.size();
510 res.mDocument.reserve( documentMetrics.blockSize );
511 res.mFragmentFonts.reserve( documentMetrics.blockSize );
512
513 for ( int blockIndex = 0; blockIndex < documentMetrics.blockSize; blockIndex++ )
514 {
515 const QgsTextBlock &block = document.at( blockIndex );
516 QgsTextBlock outputBlock;
517 outputBlock.setBlockFormat( block.blockFormat() );
518 outputBlock.reserve( block.size() );
519
520 const int fragmentSize = block.size();
521
522 BlockMetrics thisBlockMetrics;
523 thisBlockMetrics.fragmentVerticalOffsets.reserve( fragmentSize );
524 thisBlockMetrics.fragmentFonts.reserve( fragmentSize );
525 thisBlockMetrics.fragmentHorizontalAdvance.reserve( fragmentSize );
526 thisBlockMetrics.fragmentFixedHeights.reserve( fragmentSize );
527
528 thisBlockMetrics.isFirstBlock = blockIndex == 0;
529 thisBlockMetrics.isLastBlock = blockIndex == documentMetrics.blockSize - 1;
530
531 for ( int fragmentIndex = 0; fragmentIndex < fragmentSize; ++fragmentIndex )
532 {
533 const QgsTextFragment &fragment = block.at( fragmentIndex );
534 processFragment( res, format, context, documentContext, scaleFactor, documentMetrics, thisBlockMetrics, font, fragment, outputBlock );
535 }
536
537 finalizeBlock( res, format, documentMetrics, outputBlock, thisBlockMetrics );
538 }
539
540 documentMetrics.heightLabelMode -= documentMetrics.lastLineLeading;
541 documentMetrics.heightPointRectMode -= documentMetrics.lastLineLeading;
542
543 res.mDocumentSizeLabelMode = QSizeF( documentMetrics.width, documentMetrics.heightLabelMode );
544 res.mDocumentSizePointRectMode = QSizeF( documentMetrics.width, documentMetrics.heightPointRectMode );
545 res.mDocumentSizeCapHeightMode = QSizeF( documentMetrics.width, documentMetrics.heightCapHeightMode );
546 res.mDocumentSizeAscentMode = QSizeF( documentMetrics.width, documentMetrics.heightAscentMode );
547
548 // adjust baselines
549 if ( !res.mBaselineOffsetsLabelMode.isEmpty() )
550 {
551 const double labelModeBaselineAdjust = res.mBaselineOffsetsLabelMode.constLast() + res.mLastLineAscentOffset;
552 const double pointModeBaselineAdjust = res.mBaselineOffsetsPointMode.constLast();
553 for ( int i = 0; i < documentMetrics.blockSize; ++i )
554 {
555 res.mBaselineOffsetsLabelMode[i] -= labelModeBaselineAdjust;
556 res.mBaselineOffsetsPointMode[i] -= pointModeBaselineAdjust;
557 }
558 }
559
560 if ( !res.mBlockMaxCharacterWidth.isEmpty() )
561 {
562 QList< double > adjustedRightToLeftXOffsets;
563 double currentOffset = 0;
564 const int size = res.mBlockMaxCharacterWidth.size();
565
566 double widthVerticalOrientation = 0;
567 for ( int i = 0; i < size; ++i )
568 {
569 const double rightToLeftBlockMaxCharacterWidth = res.mBlockMaxCharacterWidth[size - 1 - i ];
570 const double rightToLeftLineSpacing = documentMetrics.blockVerticalLineSpacing[ size - 1 - i ];
571
572 adjustedRightToLeftXOffsets << currentOffset;
573 currentOffset += rightToLeftLineSpacing;
574
575 if ( i == size - 1 )
576 widthVerticalOrientation += rightToLeftBlockMaxCharacterWidth;
577 else
578 widthVerticalOrientation += rightToLeftLineSpacing;
579 }
580 std::reverse( adjustedRightToLeftXOffsets.begin(), adjustedRightToLeftXOffsets.end() );
581 res.mVerticalOrientationXOffsets = adjustedRightToLeftXOffsets;
582
583 res.mDocumentSizeVerticalOrientation = QSizeF( widthVerticalOrientation, documentMetrics.heightVerticalOrientation );
584 }
585
586 res.mOuterBoundsLabelMode = QRectF( documentMetrics.outerXMin, -documentMetrics.outerYMaxLabel,
587 documentMetrics.outerXMax - documentMetrics.outerXMin,
588 documentMetrics.heightLabelMode - documentMetrics.outerYMinLabel + documentMetrics.outerYMaxLabel );
589
590 return res;
591}
592
594{
595 switch ( orientation )
596 {
598 switch ( mode )
599 {
602 return mDocumentSizePointRectMode;
603
605 return mDocumentSizeCapHeightMode;
606
608 return mDocumentSizeAscentMode;
609
611 return mDocumentSizeLabelMode;
612 };
614
616 return mDocumentSizeVerticalOrientation;
618 return QSizeF(); // label mode only
619 }
620
622}
623
625{
626 switch ( orientation )
627 {
629 switch ( mode )
630 {
635 return QRectF();
636
638 return mOuterBoundsLabelMode;
639 };
641
644 return QRectF(); // label mode only
645 }
646
648}
649
650double QgsTextDocumentMetrics::blockWidth( int blockIndex ) const
651{
652 return mBlockWidths.value( blockIndex );
653}
654
655double QgsTextDocumentMetrics::blockHeight( int blockIndex ) const
656{
657 return mBlockHeights.value( blockIndex );
658}
659
661{
662 return mFirstLineCapHeight;
663}
664
666{
667 switch ( mode )
668 {
670 return mBaselineOffsetsRectMode.value( blockIndex );
672 return mBaselineOffsetsCapHeightMode.value( blockIndex );
674 return mBaselineOffsetsAscentBased.value( blockIndex );
676 return mBaselineOffsetsPointMode.value( blockIndex );
678 return mBaselineOffsetsLabelMode.value( blockIndex );
679 }
681}
682
683double QgsTextDocumentMetrics::fragmentHorizontalAdvance( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
684{
685 return mFragmentHorizontalAdvance.value( blockIndex ).value( fragmentIndex );
686}
687
688double QgsTextDocumentMetrics::fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const
689{
690 switch ( mode )
691 {
695 return mFragmentVerticalOffsetsRectMode.value( blockIndex ).value( fragmentIndex );
697 return mFragmentVerticalOffsetsPointMode.value( blockIndex ).value( fragmentIndex );
699 return mFragmentVerticalOffsetsLabelMode.value( blockIndex ).value( fragmentIndex );
700 }
702}
703
704double QgsTextDocumentMetrics::fragmentFixedHeight( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
705{
706 return mFragmentFixedHeights.value( blockIndex ).value( fragmentIndex );
707}
708
710{
711 return mVerticalOrientationXOffsets.value( blockIndex );
712}
713
715{
716 return mBlockMaxCharacterWidth.value( blockIndex );
717}
718
720{
721 return mBlockMaxDescent.value( blockIndex );
722}
723
724QFont QgsTextDocumentMetrics::fragmentFont( int blockIndex, int fragmentIndex ) const
725{
726 return mFragmentFonts.value( blockIndex ).value( fragmentIndex );
727}
728
TextLayoutMode
Text layout modes.
Definition qgis.h:2699
@ 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:2684
@ 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)
@ Points
Points (e.g., for font sizes)
@ RenderBlocking
Render and load remote sources in the same thread to ensure rendering remote sources (svg and images)...
@ WrapLines
Automatically wrap long lines of text.
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
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.
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).
Qgis::RenderContextFlags flags() const
Returns combination of flags used for rendering.
Represents a block of text consisting of one or more QgsTextFragment objects.
int size() const
Returns the number of fragments in the block.
const QgsTextBlockFormat & blockFormat() const
Returns the block formatting for the fragment.
void clear()
Clears the block, removing all its contents.
void reserve(int count)
Reserves the specified count of fragments for optimised fragment appending.
void setBlockFormat(const QgsTextBlockFormat &format)
Sets the block format for the fragment.
void append(const QgsTextFragment &fragment)
Appends a fragment to the block.
const QgsTextFragment & at(int index) const
Returns the fragment at the specified index.
bool empty() const
Returns true if the block is empty.
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 ...
QSizeF imageSize() const
Returns the image size, if the format applies to a document image 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.
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.
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,...
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...
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.
Encapsulates the context in which a text document is to be rendered.
Qgis::TextRendererFlags flags() const
Returns associated text renderer flags.
double maximumWidth() const
Returns the maximum width (in painter units) for rendered text.
Represents a document consisting of one or more QgsTextBlock objects.
const QgsTextBlock & at(int index) const
Returns the block at the specified index.
void reserve(int count)
Reserves the specified count of blocks for optimised block appending.
int size() const
Returns the number of blocks in the document.
void append(const QgsTextBlock &block)
Appends a block to the document.
Container for all settings relating to text rendering.
double lineHeight() const
Returns the line height for text.
double tabStopDistance() const
Returns the distance for tab stops.
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.
Qgis::RenderUnit tabStopDistanceUnit() const
Returns the units for the tab stop distance.
QgsMapUnitScale tabStopDistanceMapUnitScale() const
Returns the map unit scale object for the tab stop distance.
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.
void setText(const QString &text)
Sets the text content of the fragment.
QString text() const
Returns the text content of the fragment.
void setCharacterFormat(const QgsTextCharacterFormat &format)
Sets the character format for 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 isTab() const
Returns true if the fragment consists of just a tab character.
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:6571
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5917
constexpr double SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR
constexpr double SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR
QList< QFont > fragmentFonts
QList< double > fragmentFixedHeights
QList< double > fragmentHorizontalAdvance
double blockHeightUsingAscentAccountingForVerticalOffset
QList< double > fragmentVerticalOffsets
QVector< double > blockVerticalLineSpacing