QGIS API Documentation 3.41.0-Master (45a0abf3bec)
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
60
61 QVector< double > blockLeftMargin;
62 QVector< double > blockRightMargin;
63
64 double outerXMin = 0;
65 double outerXMax = 0;
66 double outerYMinLabel = 0;
67 double outerYMaxLabel = 0;
68};
69
71{
72 bool isFirstBlock = false;
73 bool isLastBlock = false;
74 double maxLineSpacing = 0;
75 double blockWidth = 0;
76 double blockXMax = 0;
85 double maxBlockAscent = 0;
86 double maxBlockDescent = 0;
87 double maxBlockMaxWidth = 0;
88 double maxBlockLeading = 0;
89
90 QList< QFont > fragmentFonts;
91 QList< double > fragmentVerticalOffsets;
92 QList< double > fragmentFixedHeights;
94
97
98 // non calculated properties
101
102 double marginTop = 0;
103 double marginBottom = 0;
104 double marginLeft = 0;
105 double marginRight = 0;
106
136};
137
138
139void QgsTextDocumentMetrics::finalizeBlock( QgsTextDocumentMetrics &res, const QgsTextFormat &, DocumentMetrics &documentMetrics, QgsTextBlock &outputBlock, BlockMetrics &metrics )
140{
141 if ( metrics.isFirstBlock )
142 {
143 documentMetrics.verticalMarginsBetweenBlocks.append( metrics.marginTop );
144 documentMetrics.verticalMarginsBetweenBlocks.append( metrics.marginBottom );
145
146 documentMetrics.currentLabelBaseline += metrics.marginTop;
147 documentMetrics.currentRectBaseline += metrics.marginTop;
148 documentMetrics.currentPointBaseline += metrics.marginTop;
149 documentMetrics.currentCapHeightBasedBaseline += metrics.marginTop;
150 documentMetrics.currentAscentBasedBaseline += metrics.marginTop;
151
152 // same logic as used in QgsTextRenderer. (?!!)
153 // needed to move bottom of text's descender to within bottom edge of label
154 res.mFirstLineAscentOffset = 0.25 * metrics.maxBlockAscentForTextFragments; // descent() is not enough
155 res.mLastLineAscentOffset = res.mFirstLineAscentOffset;
156 res.mFirstLineCapHeight = metrics.maxBlockCapHeight;
157 const double lineHeight = ( metrics.maxBlockAscent + metrics.maxBlockDescent ); // ignore +1 for baseline
158
159 // rendering labels needs special handling - in this case text should be
160 // drawn with the bottom left corner coinciding with origin, vs top left
161 // for standard text rendering. Line height is also slightly different.
162 documentMetrics.currentLabelBaseline = -res.mFirstLineAscentOffset;
163
166
167 // standard rendering - designed to exactly replicate QPainter's drawText method
168 documentMetrics.currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/;
169
170 documentMetrics.currentCapHeightBasedBaseline += res.mFirstLineCapHeight;
171 documentMetrics.currentAscentBasedBaseline += metrics.maxBlockAscent;
172
173 // standard rendering - designed to exactly replicate QPainter's drawText rect method
174 documentMetrics.currentPointBaseline = 0;
175
176 documentMetrics.heightLabelMode += metrics.blockHeightUsingAscentDescent + metrics.marginTop;
177 documentMetrics.heightPointRectMode += metrics.blockHeightUsingAscentDescent + metrics.marginTop;
178 documentMetrics.heightCapHeightMode += metrics.maxBlockCapHeight + metrics.marginTop;
179 documentMetrics.heightAscentMode += metrics.maxBlockAscent + metrics.marginTop;
180 }
181 else
182 {
183 // html vertical margins between blocks collapse and take the size of the highest margin:
184 const double verticalMarginBeforeBlock = std::max( documentMetrics.verticalMarginsBetweenBlocks.last(), metrics.marginTop );
185 documentMetrics.verticalMarginsBetweenBlocks.last() = verticalMarginBeforeBlock;
186 documentMetrics.verticalMarginsBetweenBlocks.append( metrics.marginBottom );
187
188 double thisLineHeightUsingAscentDescent = metrics.lineHeightPercentage != 0 ? ( metrics.lineHeightPercentage * ( metrics.maxBlockAscent + metrics.maxBlockDescent ) ) : metrics.lineHeightPainterUnits;
189 double thisLineHeightUsingLineSpacing = metrics.lineHeightPercentage != 0 ? ( metrics.lineHeightPercentage * metrics.maxLineSpacing ) : metrics.lineHeightPainterUnits;
190
191 thisLineHeightUsingAscentDescent = std::max( thisLineHeightUsingAscentDescent, metrics.maxBlockFixedItemHeight );
192 thisLineHeightUsingLineSpacing = std::max( thisLineHeightUsingLineSpacing, metrics.maxBlockFixedItemHeight );
193
194 documentMetrics.currentLabelBaseline += verticalMarginBeforeBlock + thisLineHeightUsingAscentDescent;
195 documentMetrics.currentRectBaseline += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
196 documentMetrics.currentPointBaseline += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
197 // using cap height??
198 documentMetrics.currentCapHeightBasedBaseline += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
199 // using ascent?
200 documentMetrics.currentAscentBasedBaseline += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
201
202 documentMetrics.heightLabelMode += verticalMarginBeforeBlock + thisLineHeightUsingAscentDescent;
203 documentMetrics.heightPointRectMode += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
204 documentMetrics.heightCapHeightMode += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
205 documentMetrics.heightAscentMode += verticalMarginBeforeBlock + thisLineHeightUsingLineSpacing;
206 if ( metrics.isLastBlock )
207 {
208 res.mLastLineAscentOffset = 0.25 * metrics.maxBlockAscentForTextFragments;
209 documentMetrics.heightLabelMode += metrics.marginBottom;
210 documentMetrics.heightPointRectMode += metrics.marginBottom;
211 documentMetrics.heightCapHeightMode += metrics.marginBottom;
212 documentMetrics.heightAscentMode += metrics.marginBottom;
213 }
214 }
215
216 documentMetrics.blockLeftMargin << metrics.marginLeft;
217 documentMetrics.blockRightMargin << metrics.marginRight;
218
219 if ( metrics.isLastBlock )
220 {
221 if ( metrics.blockYMaxAdjustLabel > metrics.maxBlockDescent )
222 documentMetrics.outerYMaxLabel = metrics.blockYMaxAdjustLabel - metrics.maxBlockDescent;
223 }
224
225 documentMetrics.blockVerticalLineSpacing << ( metrics.lineHeightPercentage != 0 ? ( metrics.maxBlockMaxWidth * metrics.lineHeightPercentage ) : metrics.lineHeightPainterUnits );
226
227 res.mBlockHeights << metrics.blockHeightUsingLineSpacing;
228
229 documentMetrics.width = std::max( documentMetrics.width, metrics.blockWidth + metrics.marginLeft + metrics.marginRight );
230 documentMetrics.outerXMax = std::max( documentMetrics.outerXMax, metrics.blockXMax );
231
232 documentMetrics.heightVerticalOrientation = std::max( documentMetrics.heightVerticalOrientation, metrics.blockHeightVerticalOrientation );
233 res.mBlockWidths << metrics.blockWidth;
234 res.mFragmentFonts << metrics.fragmentFonts;
235 res.mBaselineOffsetsLabelMode << documentMetrics.currentLabelBaseline;
236 res.mBaselineOffsetsPointMode << documentMetrics.currentPointBaseline;
237 res.mBaselineOffsetsRectMode << documentMetrics.currentRectBaseline;
238 res.mBaselineOffsetsCapHeightMode << documentMetrics.currentCapHeightBasedBaseline;
239 res.mBaselineOffsetsAscentBased << documentMetrics.currentAscentBasedBaseline;
240 res.mBlockMaxDescent << metrics.maxBlockDescent;
241 res.mBlockMaxCharacterWidth << metrics.maxBlockMaxWidth;
242 res.mFragmentVerticalOffsetsLabelMode << metrics.fragmentVerticalOffsets;
243 res.mFragmentFixedHeights << metrics.fragmentFixedHeights;
244 res.mFragmentVerticalOffsetsRectMode << metrics.fragmentVerticalOffsets;
245 res.mFragmentVerticalOffsetsPointMode << metrics.fragmentVerticalOffsets;
246 res.mFragmentHorizontalAdvance << metrics.fragmentHorizontalAdvance;
247
248 res.mDocument.append( outputBlock );
249 outputBlock.clear();
250
251 if ( !metrics.isFirstBlock )
252 documentMetrics.lastLineLeading = metrics.maxBlockLeading;
253
254 // reset metrics for next block
255 metrics.resetCalculatedStats();
256};
257
258
259void 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 )
260{
261 if ( fragment.isTab() )
262 {
263 // special handling for tab characters
264 double nextTabStop = 0;
265 if ( !documentMetrics.tabStopDistancesPainterUnits.isEmpty() )
266 {
267 // if we don't find a tab stop before the current length of line, we just ignore the tab character entirely
268 nextTabStop = thisBlockMetrics.blockXMax;
269 for ( const double tabStop : std::as_const( documentMetrics.tabStopDistancesPainterUnits ) )
270 {
271 if ( tabStop >= thisBlockMetrics.blockXMax )
272 {
273 nextTabStop = tabStop;
274 break;
275 }
276 }
277 }
278 else
279 {
280 nextTabStop = ( std::floor( thisBlockMetrics.blockXMax / documentMetrics.tabStopDistancePainterUnits ) + 1 ) * documentMetrics.tabStopDistancePainterUnits;
281 }
282 const double fragmentWidth = nextTabStop - thisBlockMetrics.blockXMax;
283
284 thisBlockMetrics.blockWidth += fragmentWidth;
285 thisBlockMetrics.blockXMax += fragmentWidth;
286
287 thisBlockMetrics.fragmentVerticalOffsets << 0;
288 thisBlockMetrics.fragmentHorizontalAdvance << fragmentWidth;
289 thisBlockMetrics.fragmentFixedHeights << -1;
290 thisBlockMetrics.fragmentFonts << QFont();
291 currentOutputBlock.append( fragment );
292 }
293 else
294 {
295 const QgsTextCharacterFormat &fragmentFormat = fragment.characterFormat();
296
297 double fragmentHeightForVerticallyOffsetText = 0;
298 double fragmentYMaxAdjust = 0;
299
300 QFont updatedFont = font;
301 fragmentFormat.updateFontForFormat( updatedFont, context, scaleFactor );
302
303 QFontMetricsF fm( updatedFont );
304
305 // 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
306 // in order to respect text wrapping
307 if ( thisBlockMetrics.isFirstNonTabFragment )
308 thisBlockMetrics.previousNonSuperSubScriptFont = updatedFont;
309
310 double fragmentVerticalOffset = 0;
311 if ( fragmentFormat.hasVerticalAlignmentSet() )
312 {
313 switch ( fragmentFormat.verticalAlignment() )
314 {
316 thisBlockMetrics.previousNonSuperSubScriptFont = updatedFont;
317 break;
318
320 {
321 const QFontMetricsF previousFM( thisBlockMetrics.previousNonSuperSubScriptFont );
322
323 if ( fragmentFormat.fontPointSize() < 0 )
324 {
325 // if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
326 // this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
327 // the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
328 // that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
329 // which we should respect
330 updatedFont.setPixelSize( static_cast< int >( std::round( updatedFont.pixelSize() * QgsTextRenderer::SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
331 fm = QFontMetricsF( updatedFont );
332 }
333
334 // to match Qt behavior in QTextLine::draw
335 fragmentVerticalOffset = -( previousFM.ascent() + previousFM.descent() ) * SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
336
337 // note -- this should really be fm.ascent(), not fm.capHeight() -- but in practice the ascent of most fonts is too large
338 // and causes unnecessarily large bounding boxes of vertically offset text!
339 fragmentHeightForVerticallyOffsetText = -fragmentVerticalOffset + fm.capHeight() / scaleFactor;
340 break;
341 }
342
344 {
345 const QFontMetricsF previousFM( thisBlockMetrics.previousNonSuperSubScriptFont );
346
347 if ( fragmentFormat.fontPointSize() < 0 )
348 {
349 // see above!!
350 updatedFont.setPixelSize( static_cast< int>( std::round( updatedFont.pixelSize() * QgsTextRenderer::SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR ) ) );
351 fm = QFontMetricsF( updatedFont );
352 }
353
354 // to match Qt behavior in QTextLine::draw
355 fragmentVerticalOffset = ( previousFM.ascent() + previousFM.descent() ) * SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
356
357 fragmentYMaxAdjust = fragmentVerticalOffset + fm.descent() / scaleFactor;
358 break;
359 }
360 }
361 }
362 else
363 {
364 thisBlockMetrics.previousNonSuperSubScriptFont = updatedFont;
365 }
366
367 auto updateCommonBlockMetrics = [ &fragmentVerticalOffset,
368 &fragmentYMaxAdjust,
369 &fragmentHeightForVerticallyOffsetText,
370 &updatedFont,
371 &fm,
372 scaleFactor]( BlockMetrics & thisBlockMetrics, double fragmentWidth, const QgsTextFragment & fragment )
373 {
374 thisBlockMetrics.fragmentVerticalOffsets << fragmentVerticalOffset;
375 thisBlockMetrics.blockYMaxAdjustLabel = std::max( thisBlockMetrics.blockYMaxAdjustLabel, fragmentYMaxAdjust );
376 thisBlockMetrics.blockHeightUsingAscentAccountingForVerticalOffset = std::max( std::max( thisBlockMetrics.maxBlockAscent, fragmentHeightForVerticallyOffsetText ), thisBlockMetrics.blockHeightUsingAscentAccountingForVerticalOffset );
377
378 thisBlockMetrics.fragmentHorizontalAdvance << fragmentWidth;
379
380 thisBlockMetrics.blockWidth += fragmentWidth;
381 thisBlockMetrics.blockXMax += fragmentWidth;
382
383 thisBlockMetrics.fragmentFonts << updatedFont;
384
385 const double verticalOrientationFragmentHeight = thisBlockMetrics.isFirstNonTabFragment ? ( fm.ascent() / scaleFactor * fragment.text().size() + ( fragment.text().size() - 1 ) * updatedFont.letterSpacing() / scaleFactor )
386 : ( fragment.text().size() * ( fm.ascent() / scaleFactor + updatedFont.letterSpacing() / scaleFactor ) );
387 thisBlockMetrics.blockHeightVerticalOrientation += verticalOrientationFragmentHeight;
388
389 thisBlockMetrics.isFirstNonTabFragment = false;
390 };
391
392 // calculate width of fragment
393 if ( fragment.isImage() )
394 {
395 double imageHeight = 0;
396 double imageWidth = 0;
397 if ( ( qgsDoubleNear( fragmentFormat.imageSize().width(), 0 ) || fragmentFormat.imageSize().width() < 0 )
398 && ( qgsDoubleNear( fragmentFormat.imageSize().height(), 0 ) || fragmentFormat.imageSize().height() < 0 ) )
399 {
400 // use original image size
401 const QSize imageSize = QgsApplication::imageCache()->originalSize( fragmentFormat.imagePath(), context.flags() & Qgis::RenderContextFlag::RenderBlocking );
402 // TODO: maybe there's more optimal logic we could use here, but for now we assume 96dpi image resolution...
403 const QSizeF originalSizeMmAt96Dpi = imageSize / 3.7795275590551185;
404 const double pixelsPerMm = context.scaleFactor();
405 imageWidth = originalSizeMmAt96Dpi.width() * pixelsPerMm;
406 imageHeight = originalSizeMmAt96Dpi.height() * pixelsPerMm;
407 }
408 else if ( ( qgsDoubleNear( fragmentFormat.imageSize().width(), 0 ) || fragmentFormat.imageSize().width() < 0 ) )
409 {
410 // height specified, calculate width
411 const QSize originalImageSize = QgsApplication::imageCache()->originalSize( fragmentFormat.imagePath(), context.flags() & Qgis::RenderContextFlag::RenderBlocking );
412 imageHeight = context.convertToPainterUnits( fragmentFormat.imageSize().height(), Qgis::RenderUnit::Points );
413 imageWidth = originalImageSize.width() * imageHeight / originalImageSize.height();
414 }
415 else if ( ( qgsDoubleNear( fragmentFormat.imageSize().height(), 0 ) || fragmentFormat.imageSize().height() < 0 ) )
416 {
417 // width specified, calculate height
418 const QSize originalImageSize = QgsApplication::imageCache()->originalSize( fragmentFormat.imagePath(), context.flags() & Qgis::RenderContextFlag::RenderBlocking );
419 imageWidth = context.convertToPainterUnits( fragmentFormat.imageSize().width(), Qgis::RenderUnit::Points );
420 imageHeight = originalImageSize.height() * imageWidth / originalImageSize.width();
421 }
422 else
423 {
424 imageWidth = context.convertToPainterUnits( fragmentFormat.imageSize().width(), Qgis::RenderUnit::Points );
425 imageHeight = context.convertToPainterUnits( fragmentFormat.imageSize().height(), Qgis::RenderUnit::Points );
426 }
427
428 // do we need to move this image fragment to a new block to respect wrapping?
429 if ( documentContext.flags() & Qgis::TextRendererFlag::WrapLines && documentContext.maximumWidth() > 0
430 && ( thisBlockMetrics.blockXMax + imageWidth > documentContext.maximumWidth() )
431 && !currentOutputBlock.empty() )
432 {
433 // yep, need to wrap before the image
434 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
435 thisBlockMetrics.isFirstBlock = false;
436 }
437
438 // we consider the whole image as ascent, and descent as 0
439 thisBlockMetrics.blockHeightUsingAscentDescent = std::max( thisBlockMetrics.blockHeightUsingAscentDescent, imageHeight + fm.descent() / scaleFactor );
440 thisBlockMetrics.blockHeightUsingLineSpacing = std::max( thisBlockMetrics.blockHeightUsingLineSpacing, imageHeight + fm.leading() );
441
442 thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, imageHeight );
443 thisBlockMetrics.maxBlockCapHeight = std::max( thisBlockMetrics.maxBlockCapHeight, imageHeight );
444 thisBlockMetrics.maxLineSpacing = std::max( thisBlockMetrics.maxLineSpacing, imageHeight + fm.leading() / scaleFactor );
445 thisBlockMetrics.maxBlockLeading = std::max( thisBlockMetrics.maxBlockLeading, fm.leading() / scaleFactor );
446 thisBlockMetrics.maxBlockMaxWidth = std::max( thisBlockMetrics.maxBlockMaxWidth, imageWidth );
447 thisBlockMetrics.maxBlockFixedItemHeight = std::max( thisBlockMetrics.maxBlockFixedItemHeight, imageHeight );
448 thisBlockMetrics.fragmentFixedHeights << imageHeight;
449 updateCommonBlockMetrics( thisBlockMetrics, imageWidth, fragment );
450 currentOutputBlock.append( fragment );
451 }
452 else
453 {
454 const double fragmentHeightUsingAscentDescent = ( fm.ascent() + fm.descent() ) / scaleFactor;
455 const double fragmentHeightUsingLineSpacing = fm.lineSpacing() / scaleFactor;
456
457 auto finalizeTextFragment = [fragmentHeightUsingAscentDescent,
458 fragmentHeightUsingLineSpacing,
459 &fm,
460 scaleFactor,
461 &currentOutputBlock,
462 &updateCommonBlockMetrics
463 ]( BlockMetrics & thisBlockMetrics, const QgsTextFragment & fragment, double fragmentWidth )
464 {
465 thisBlockMetrics.blockHeightUsingAscentDescent = std::max( thisBlockMetrics.blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent );
466
467 thisBlockMetrics.blockHeightUsingLineSpacing = std::max( thisBlockMetrics.blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
468 thisBlockMetrics.maxBlockAscent = std::max( thisBlockMetrics.maxBlockAscent, fm.ascent() / scaleFactor );
469 thisBlockMetrics.maxBlockAscentForTextFragments = std::max( thisBlockMetrics.maxBlockAscentForTextFragments, fm.ascent() / scaleFactor );
470
471 thisBlockMetrics.maxBlockCapHeight = std::max( thisBlockMetrics.maxBlockCapHeight, fm.capHeight() / scaleFactor );
472
473 thisBlockMetrics.maxBlockDescent = std::max( thisBlockMetrics.maxBlockDescent, fm.descent() / scaleFactor );
474 thisBlockMetrics.maxBlockMaxWidth = std::max( thisBlockMetrics.maxBlockMaxWidth, fm.maxWidth() / scaleFactor );
475
476 if ( ( fm.lineSpacing() / scaleFactor ) > thisBlockMetrics.maxLineSpacing )
477 {
478 thisBlockMetrics.maxLineSpacing = fm.lineSpacing() / scaleFactor;
479 thisBlockMetrics.maxBlockLeading = fm.leading() / scaleFactor;
480 }
481 thisBlockMetrics.fragmentFixedHeights << -1;
482 updateCommonBlockMetrics( thisBlockMetrics, fragmentWidth, fragment );
483 currentOutputBlock.append( fragment );
484 };
485
486 double fragmentWidth = fm.horizontalAdvance( fragment.text() ) / scaleFactor;
487
488 // do we need to split this fragment to respect wrapping?
489 if ( documentContext.flags() & Qgis::TextRendererFlag::WrapLines && documentContext.maximumWidth() > 0
490 && ( thisBlockMetrics.blockXMax + fragmentWidth > documentContext.maximumWidth() ) )
491 {
492 // yep, need to split the fragment!
493
494 //first step is to identify words which must be on their own line (too long to fit)
495 const QStringList words = fragment.text().split( ' ' );
496 QStringList linesToProcess;
497 QStringList wordsInCurrentLine;
498 double remainingWidthInCurrentLine = documentContext.maximumWidth() - thisBlockMetrics.blockXMax;
499 for ( const QString &word : words )
500 {
501 const double wordWidth = fm.horizontalAdvance( word ) / scaleFactor;
502 if ( wordWidth > remainingWidthInCurrentLine )
503 {
504 //too long to fit
505 if ( !wordsInCurrentLine.isEmpty() )
506 linesToProcess << wordsInCurrentLine.join( ' ' );
507 wordsInCurrentLine.clear();
508 linesToProcess << word;
509 remainingWidthInCurrentLine = documentContext.maximumWidth();
510 }
511 else
512 {
513 wordsInCurrentLine.append( word );
514 }
515 }
516 if ( !wordsInCurrentLine.isEmpty() )
517 linesToProcess << wordsInCurrentLine.join( ' ' );
518
519 remainingWidthInCurrentLine = documentContext.maximumWidth() - thisBlockMetrics.blockXMax;
520 for ( int lineIndex = 0; lineIndex < linesToProcess.size(); ++lineIndex )
521 {
522 QString remainingText = linesToProcess.at( lineIndex );
523 int lastPos = remainingText.lastIndexOf( ' ' );
524 while ( lastPos > -1 )
525 {
526 //check if remaining text is short enough to go in one line
527 if ( ( fm.horizontalAdvance( remainingText ) / scaleFactor ) <= remainingWidthInCurrentLine )
528 {
529 break;
530 }
531
532 const double widthTextToLastPos = fm.horizontalAdvance( remainingText.left( lastPos ) ) / scaleFactor;
533 if ( widthTextToLastPos <= remainingWidthInCurrentLine )
534 {
535 QgsTextFragment thisLineFragment;
536 thisLineFragment.setCharacterFormat( fragment.characterFormat() );
537 thisLineFragment.setText( remainingText.left( lastPos ) );
538 finalizeTextFragment( thisBlockMetrics, thisLineFragment, widthTextToLastPos );
539 // move to new block
540 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
541 thisBlockMetrics.isFirstBlock = false;
542 remainingWidthInCurrentLine = documentContext.maximumWidth();
543 remainingText = remainingText.mid( lastPos + 1 );
544 lastPos = 0;
545 }
546 lastPos = remainingText.lastIndexOf( ' ', lastPos - 1 );
547 }
548
549 // if too big, and block is not empty, then flush current block first
550 if ( ( fm.horizontalAdvance( remainingText ) / scaleFactor ) > remainingWidthInCurrentLine && !currentOutputBlock.empty() )
551 {
552 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
553 thisBlockMetrics.isFirstBlock = false;
554 remainingWidthInCurrentLine = documentContext.maximumWidth();
555 }
556
557 QgsTextFragment thisLineFragment;
558 thisLineFragment.setCharacterFormat( fragment.characterFormat() );
559 thisLineFragment.setText( remainingText );
560 finalizeTextFragment( thisBlockMetrics, thisLineFragment, fm.horizontalAdvance( remainingText ) / scaleFactor );
561
562 if ( lineIndex < linesToProcess.size() - 1 )
563 {
564 // start new block if we aren't at the last line
565 finalizeBlock( res, format, documentMetrics, currentOutputBlock, thisBlockMetrics );
566 thisBlockMetrics.isFirstBlock = false;
567 remainingWidthInCurrentLine = documentContext.maximumWidth();
568 }
569
570 thisBlockMetrics.isFirstBlock = false;
571 }
572 }
573 else
574 {
575 // simple case, no wrapping
576 finalizeTextFragment( thisBlockMetrics, fragment, fragmentWidth );
577 }
578 }
579 }
580}
581
582QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor, const QgsTextDocumentRenderContext &documentContext )
583{
585
586 const QFont font = format.scaledFont( context, scaleFactor, &res.mIsNullSize );
587 if ( res.isNullFontSize() )
588 return res;
589
590 DocumentMetrics documentMetrics;
591
592 // for absolute line heights
593 const double documentLineHeightPainterUnits = context.convertToPainterUnits( format.lineHeight(), format.lineHeightUnit() );
594
596 ? format.tabStopDistance() * font.pixelSize() / scaleFactor
598
599 const QList< QgsTextFormat::Tab > tabPositions = format.tabPositions();
600 documentMetrics.tabStopDistancesPainterUnits.reserve( tabPositions.size() );
601 for ( const QgsTextFormat::Tab &tab : tabPositions )
602 {
603 documentMetrics.tabStopDistancesPainterUnits.append(
605 ? tab.position() * font.pixelSize() / scaleFactor
606 : context.convertToPainterUnits( tab.position(), format.tabStopDistanceUnit(), format.tabStopDistanceMapUnitScale() )
607 );
608 }
609
610 documentMetrics.blockSize = document.size();
611 res.mDocument.reserve( documentMetrics.blockSize );
612 res.mFragmentFonts.reserve( documentMetrics.blockSize );
613
614 for ( int blockIndex = 0; blockIndex < documentMetrics.blockSize; blockIndex++ )
615 {
616 const QgsTextBlock &block = document.at( blockIndex );
617 QgsTextBlock outputBlock;
618 outputBlock.setBlockFormat( block.blockFormat() );
619 outputBlock.reserve( block.size() );
620
621 const int fragmentSize = block.size();
622
623 BlockMetrics thisBlockMetrics;
624 thisBlockMetrics.lineHeightPainterUnits = documentLineHeightPainterUnits;
625 // apply block line height if set
626 if ( !std::isnan( block.blockFormat().lineHeightPercentage() ) )
627 {
628 thisBlockMetrics.lineHeightPercentage = block.blockFormat().lineHeightPercentage();
629 }
630 else if ( !std::isnan( block.blockFormat().lineHeight() ) )
631 {
633 }
634 else if ( format.lineHeightUnit() == Qgis::RenderUnit::Percentage )
635 {
636 thisBlockMetrics.lineHeightPercentage = format.lineHeight();
637 }
638
639 thisBlockMetrics.fragmentVerticalOffsets.reserve( fragmentSize );
640 thisBlockMetrics.fragmentFonts.reserve( fragmentSize );
641 thisBlockMetrics.fragmentHorizontalAdvance.reserve( fragmentSize );
642 thisBlockMetrics.fragmentFixedHeights.reserve( fragmentSize );
643
644 thisBlockMetrics.isFirstBlock = blockIndex == 0;
645 thisBlockMetrics.isLastBlock = blockIndex == documentMetrics.blockSize - 1;
646
647 thisBlockMetrics.marginTop = context.convertToPainterUnits(
648 !std::isnan( block.blockFormat().margins().top() ) ? block.blockFormat().margins().top() : 0, Qgis::RenderUnit::Points );
649 thisBlockMetrics.marginBottom = context.convertToPainterUnits(
650 !std::isnan( block.blockFormat().margins().bottom() ) ? block.blockFormat().margins().bottom() : 0, Qgis::RenderUnit::Points );
651 thisBlockMetrics.marginLeft = context.convertToPainterUnits(
652 !std::isnan( block.blockFormat().margins().left() ) ? block.blockFormat().margins().left() : 0, Qgis::RenderUnit::Points );
653 thisBlockMetrics.marginRight = context.convertToPainterUnits(
654 !std::isnan( block.blockFormat().margins().right() ) ? block.blockFormat().margins().right() : 0, Qgis::RenderUnit::Points );
655
656 for ( int fragmentIndex = 0; fragmentIndex < fragmentSize; ++fragmentIndex )
657 {
658 const QgsTextFragment &fragment = block.at( fragmentIndex );
659 processFragment( res, format, context, documentContext, scaleFactor, documentMetrics, thisBlockMetrics, font, fragment, outputBlock );
660 }
661
662 finalizeBlock( res, format, documentMetrics, outputBlock, thisBlockMetrics );
663 }
664
665 documentMetrics.heightLabelMode -= documentMetrics.lastLineLeading;
666 documentMetrics.heightPointRectMode -= documentMetrics.lastLineLeading;
667
668 res.mDocumentSizeLabelMode = QSizeF( documentMetrics.width, documentMetrics.heightLabelMode );
669 res.mDocumentSizePointRectMode = QSizeF( documentMetrics.width, documentMetrics.heightPointRectMode );
670 res.mDocumentSizeCapHeightMode = QSizeF( documentMetrics.width, documentMetrics.heightCapHeightMode );
671 res.mDocumentSizeAscentMode = QSizeF( documentMetrics.width, documentMetrics.heightAscentMode );
672
673 // adjust baselines
674 if ( !res.mBaselineOffsetsLabelMode.isEmpty() )
675 {
676 const double labelModeBaselineAdjust = res.mBaselineOffsetsLabelMode.constLast() + res.mLastLineAscentOffset;
677 const double pointModeBaselineAdjust = res.mBaselineOffsetsPointMode.constLast();
678 for ( int i = 0; i < documentMetrics.blockSize; ++i )
679 {
680 res.mBaselineOffsetsLabelMode[i] -= labelModeBaselineAdjust;
681 res.mBaselineOffsetsPointMode[i] -= pointModeBaselineAdjust;
682 }
683 }
684
685 if ( !res.mBlockMaxCharacterWidth.isEmpty() )
686 {
687 QList< double > adjustedRightToLeftXOffsets;
688 double currentOffset = 0;
689 const int size = res.mBlockMaxCharacterWidth.size();
690
691 double widthVerticalOrientation = 0;
692 for ( int i = 0; i < size; ++i )
693 {
694 const double rightToLeftBlockMaxCharacterWidth = res.mBlockMaxCharacterWidth[size - 1 - i ];
695 const double rightToLeftLineSpacing = documentMetrics.blockVerticalLineSpacing[ size - 1 - i ];
696
697 adjustedRightToLeftXOffsets << currentOffset;
698 currentOffset += rightToLeftLineSpacing;
699
700 if ( i == size - 1 )
701 widthVerticalOrientation += rightToLeftBlockMaxCharacterWidth;
702 else
703 widthVerticalOrientation += rightToLeftLineSpacing;
704 }
705 std::reverse( adjustedRightToLeftXOffsets.begin(), adjustedRightToLeftXOffsets.end() );
706 res.mVerticalOrientationXOffsets = adjustedRightToLeftXOffsets;
707
708 res.mDocumentSizeVerticalOrientation = QSizeF( widthVerticalOrientation, documentMetrics.heightVerticalOrientation );
709 }
710
711 res.mVerticalMarginsBetweenBlocks = documentMetrics.verticalMarginsBetweenBlocks;
712 res.mLeftBlockMargins = documentMetrics.blockLeftMargin;
713 res.mRightBlockMargins = documentMetrics.blockRightMargin;
714
715 res.mOuterBoundsLabelMode = QRectF( documentMetrics.outerXMin, -documentMetrics.outerYMaxLabel,
716 documentMetrics.outerXMax - documentMetrics.outerXMin,
717 documentMetrics.heightLabelMode - documentMetrics.outerYMinLabel + documentMetrics.outerYMaxLabel );
718
719 return res;
720}
721
723{
724 switch ( orientation )
725 {
727 switch ( mode )
728 {
731 return mDocumentSizePointRectMode;
732
734 return mDocumentSizeCapHeightMode;
735
737 return mDocumentSizeAscentMode;
738
740 return mDocumentSizeLabelMode;
741 };
743
745 return mDocumentSizeVerticalOrientation;
747 return QSizeF(); // label mode only
748 }
749
751}
752
754{
755 switch ( orientation )
756 {
758 switch ( mode )
759 {
764 return QRectF();
765
767 return mOuterBoundsLabelMode;
768 };
770
773 return QRectF(); // label mode only
774 }
775
777}
778
779double QgsTextDocumentMetrics::blockWidth( int blockIndex ) const
780{
781 return mBlockWidths.value( blockIndex );
782}
783
784double QgsTextDocumentMetrics::blockHeight( int blockIndex ) const
785{
786 return mBlockHeights.value( blockIndex );
787}
788
790{
791 return mFirstLineCapHeight;
792}
793
795{
796 double verticalAdjustmentForBlockMargins = 0;
797 for ( int i = 0; i < blockIndex; ++i )
798 {
799 double marginBeforeBlock = 0;
800 verticalAdjustmentForBlockMargins += marginBeforeBlock;
801 }
802
803 switch ( mode )
804 {
806 return mBaselineOffsetsRectMode.value( blockIndex ) + verticalAdjustmentForBlockMargins;
808 return mBaselineOffsetsCapHeightMode.value( blockIndex ) + verticalAdjustmentForBlockMargins;
810 return mBaselineOffsetsAscentBased.value( blockIndex ) + verticalAdjustmentForBlockMargins;
812 return mBaselineOffsetsPointMode.value( blockIndex ) + verticalAdjustmentForBlockMargins;
814 return mBaselineOffsetsLabelMode.value( blockIndex ) + verticalAdjustmentForBlockMargins;
815 }
817}
818
819double QgsTextDocumentMetrics::fragmentHorizontalAdvance( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
820{
821 return mFragmentHorizontalAdvance.value( blockIndex ).value( fragmentIndex );
822}
823
824double QgsTextDocumentMetrics::fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const
825{
826 switch ( mode )
827 {
831 return mFragmentVerticalOffsetsRectMode.value( blockIndex ).value( fragmentIndex );
833 return mFragmentVerticalOffsetsPointMode.value( blockIndex ).value( fragmentIndex );
835 return mFragmentVerticalOffsetsLabelMode.value( blockIndex ).value( fragmentIndex );
836 }
838}
839
840double QgsTextDocumentMetrics::fragmentFixedHeight( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode ) const
841{
842 return mFragmentFixedHeights.value( blockIndex ).value( fragmentIndex );
843}
844
846{
847 return mVerticalOrientationXOffsets.value( blockIndex );
848}
849
851{
852 return mBlockMaxCharacterWidth.value( blockIndex );
853}
854
856{
857 return mBlockMaxDescent.value( blockIndex );
858}
859
860QFont QgsTextDocumentMetrics::fragmentFont( int blockIndex, int fragmentIndex ) const
861{
862 return mFragmentFonts.value( blockIndex ).value( fragmentIndex );
863}
864
866{
867 if ( blockIndex < 0 )
868 return mVerticalMarginsBetweenBlocks.value( 0 );
869
870 return mVerticalMarginsBetweenBlocks.value( blockIndex + 1 );
871}
872
873double QgsTextDocumentMetrics::blockLeftMargin( int blockIndex ) const
874{
875 return mLeftBlockMargins.value( blockIndex );
876}
877
878double QgsTextDocumentMetrics::blockRightMargin( int blockIndex ) const
879{
880 return mRightBlockMargins.value( blockIndex );
881}
882
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.
double top() const
Returns the top margin.
Definition qgsmargins.h:77
double right() const
Returns the right margin.
Definition qgsmargins.h:83
double bottom() const
Returns the bottom margin.
Definition qgsmargins.h:89
double left() const
Returns the left margin.
Definition qgsmargins.h:71
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.
double lineHeight() const
Returns the line height in points, or NaN if the line height is not set and should be auto calculated...
double lineHeightPercentage() const
Returns the line height percentage size (as fraction of font size from 0.0 to 1.0),...
QgsMargins margins() const
Returns the block margins, in points.
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 blockRightMargin(int blockIndex) const
Returns the margin for the right side of the specified block index.
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 blockLeftMargin(int blockIndex) const
Returns the margin for the left side of the specified block 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 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.
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.
Defines a tab position for a text format.
Container for all settings relating to text rendering.
QList< QgsTextFormat::Tab > tabPositions() const
Returns the list of tab positions for tab stops.
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:6612
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5958
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
QList< double > tabStopDistancesPainterUnits
QVector< double > blockRightMargin
QVector< double > blockLeftMargin
QVector< double > blockVerticalLineSpacing
QVector< double > verticalMarginsBetweenBlocks
Calculated vertical margins between blocks.