Add support for super and subscript HTML formatting in text renderer

This allows for either:

- <sup>superscript</sup> / <sub>subscript</sub> components in text,
where the text will be vertically super or subscript aligned
and automatically sized to 2/3rd of the parent font size. Users
can also set a fixed font size for the super/sub script by
including css rules, e.g. <sup style="font-size:33pt">super</sup>

- "vertical-align: super" or "vertical-align: sub" CSS formatting
rules in any other HTML element

Sponsored by OSGEO UK
This commit is contained in:
Nyall Dawson 2022-11-05 11:14:25 +10:00
parent 79b809a585
commit a7c39ffdc7
13 changed files with 340 additions and 15 deletions

View File

@ -49,6 +49,19 @@ Returns ``True`` if the metrics could not be calculated because the text format
QSizeF documentSize( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const; QSizeF documentSize( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const;
%Docstring %Docstring
Returns the overall size of the document. Returns the overall size of the document.
%End
QRectF outerBounds( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const;
%Docstring
Returns the outer bounds of the document, which is the :py:func:`~QgsTextDocumentMetrics.documentSize` adjusted to account
for any text elements which fall outside of the usual document margins (such as super or
sub script elements)
.. warning::
Currently this is only supported for the Qgis.TextLayoutMode.Labeling mode.
.. versionadded:: 3.30
%End %End
double blockWidth( int blockIndex ) const; double blockWidth( int blockIndex ) const;
@ -70,6 +83,14 @@ Returns the offset from the top of the document to the text baseline for the giv
%Docstring %Docstring
Returns the horizontal advance of the fragment at the specified block and fragment index. Returns the horizontal advance of the fragment at the specified block and fragment index.
.. versionadded:: 3.30
%End
double fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the vertical offset from a text block's baseline which should be applied
to the fragment at the specified index within that block.
.. versionadded:: 3.30 .. versionadded:: 3.30
%End %End

View File

@ -1460,7 +1460,7 @@ bool QgsPalLayerSettings::checkMinimumSizeMM( const QgsRenderContext &ct, const
return QgsPalLabeling::checkMinimumSizeMM( ct, geom, minSize ); return QgsPalLabeling::checkMinimumSizeMM( ct, geom, minSize );
} }
void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f, QgsRenderContext *context, double *rotatedLabelX, double *rotatedLabelY, QgsTextDocument *document, QgsTextDocumentMetrics *documentMetrics ) void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f, QgsRenderContext *context, double *rotatedLabelX, double *rotatedLabelY, QgsTextDocument *document, QgsTextDocumentMetrics *documentMetrics, QRectF *outerBounds )
{ {
if ( !fm || !f ) if ( !fm || !f )
{ {
@ -1714,6 +1714,16 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QSt
*rotatedLabelY = rh * uPP; *rotatedLabelY = rh * uPP;
} }
#endif #endif
if ( outerBounds && documentMetrics )
{
const QRectF outerBoundsPixels = documentMetrics->outerBounds( Qgis::TextLayoutMode::Labeling, orientation );
*outerBounds = QRectF( outerBoundsPixels.left() * uPP,
outerBoundsPixels.top() * uPP,
outerBoundsPixels.width() * uPP,
outerBoundsPixels.height() * uPP );
}
} }
void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext &context ) void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext &context )
@ -2018,15 +2028,16 @@ std::unique_ptr<QgsLabelFeature> QgsPalLayerSettings::registerFeatureWithDetails
QgsTextDocument doc; QgsTextDocument doc;
QgsTextDocumentMetrics documentMetrics; QgsTextDocumentMetrics documentMetrics;
QRectF outerBounds;
if ( format().allowHtmlFormatting() && !labelText.isEmpty() ) if ( format().allowHtmlFormatting() && !labelText.isEmpty() )
{ {
doc = QgsTextDocument::fromHtml( QStringList() << labelText ); doc = QgsTextDocument::fromHtml( QStringList() << labelText );
// also applies the line split to doc and calculates document metrics! // also applies the line split to doc and calculates document metrics!
calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, &doc, &documentMetrics ); calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, &doc, &documentMetrics, &outerBounds );
} }
else else
{ {
calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, nullptr, nullptr ); calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, nullptr, nullptr, &outerBounds );
} }
// maximum angle between curved label characters (hardcoded defaults used in QGIS <2.0) // maximum angle between curved label characters (hardcoded defaults used in QGIS <2.0)
@ -2668,6 +2679,11 @@ std::unique_ptr<QgsLabelFeature> QgsPalLayerSettings::registerFeatureWithDetails
obstacleGeometry.boundingBox().height() ) ); obstacleGeometry.boundingBox().height() ) );
} }
if ( outerBounds.left() != 0 || outerBounds.top() != 0 || !qgsDoubleNear( outerBounds.width(), labelWidth ) || !qgsDoubleNear( outerBounds.height(), labelHeight ) )
{
labelFeature->setOuterBounds( outerBounds );
}
//set label's visual margin so that top visual margin is the leading, and bottom margin is the font's descent //set label's visual margin so that top visual margin is the leading, and bottom margin is the font's descent
//this makes labels align to the font's baseline or highest character //this makes labels align to the font's baseline or highest character
double topMargin = std::max( 0.25 * labelFontMetrics->ascent(), 0.0 ); double topMargin = std::max( 0.25 * labelFontMetrics->ascent(), 0.0 );

View File

@ -758,7 +758,7 @@ class CORE_EXPORT QgsPalLayerSettings
*/ */
#ifndef SIP_RUN #ifndef SIP_RUN
void calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f = nullptr, QgsRenderContext *context = nullptr, double *rotatedLabelX SIP_OUT = nullptr, double *rotatedLabelY SIP_OUT = nullptr, void calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f = nullptr, QgsRenderContext *context = nullptr, double *rotatedLabelX SIP_OUT = nullptr, double *rotatedLabelY SIP_OUT = nullptr,
QgsTextDocument *document = nullptr, QgsTextDocumentMetrics *documentMetrics = nullptr ); QgsTextDocument *document = nullptr, QgsTextDocumentMetrics *documentMetrics = nullptr, QRectF *outerBounds = nullptr );
#else #else
void calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f = nullptr, QgsRenderContext *context = nullptr, double *rotatedLabelX SIP_OUT = nullptr, double *rotatedLabelY SIP_OUT = nullptr ); void calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f = nullptr, QgsRenderContext *context = nullptr, double *rotatedLabelX SIP_OUT = nullptr, double *rotatedLabelY SIP_OUT = nullptr );
#endif #endif

View File

@ -556,7 +556,7 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
painter->drawRect( rect ); painter->drawRect( rect );
painter->setPen( QColor( 0, 0, 0, 120 ) ); painter->setPen( QColor( 0, 0, 0, 60 ) );
const QgsMargins &margins = label->getFeaturePart()->feature()->visualMargin(); const QgsMargins &margins = label->getFeaturePart()->feature()->visualMargin();
if ( margins.top() > 0 ) if ( margins.top() > 0 )
{ {
@ -584,7 +584,10 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
outerBoundsPt2.x() - outerBoundsPt1.x(), outerBoundsPt2.x() - outerBoundsPt1.x(),
outerBoundsPt2.y() - outerBoundsPt1.y() ); outerBoundsPt2.y() - outerBoundsPt1.y() );
painter->setPen( QColor( 255, 0, 255, 140 ) ); QPen pen( QColor( 255, 0, 255, 140 ) );
pen.setCosmetic( true );
pen.setWidth( 1 );
painter->setPen( pen );
painter->drawRect( outerBoundsPixel ); painter->drawRect( outerBoundsPixel );
} }
@ -594,13 +597,39 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q
const QgsTextDocument &document = textFeature->document(); const QgsTextDocument &document = textFeature->document();
const int blockCount = document.size(); const int blockCount = document.size();
double prevBlockBaseline = rect.bottom() - rect.top();
// draw block baselines // draw block baselines
painter->setPen( QColor( 0, 0, 255, 220 ) );
for ( int blockIndex = 0; blockIndex < blockCount; ++blockIndex ) for ( int blockIndex = 0; blockIndex < blockCount; ++blockIndex )
{ {
const double blockBaseLine = metrics.baselineOffset( blockIndex, Qgis::TextLayoutMode::Labeling ); const double blockBaseLine = metrics.baselineOffset( blockIndex, Qgis::TextLayoutMode::Labeling );
painter->drawLine( QPointF( rect.left(), rect.top() + blockBaseLine ),
QPointF( rect.right(), rect.top() + blockBaseLine ) ); const QgsTextBlock &block = document.at( blockIndex );
const int fragmentCount = block.size();
double left = 0;
for ( int fragmentIndex = 0; fragmentIndex < fragmentCount; ++fragmentIndex )
{
const double fragmentVerticalOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, Qgis::TextLayoutMode::Labeling );
const double right = left + metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, Qgis::TextLayoutMode::Labeling );
if ( fragmentIndex > 0 )
{
QPen pen( QColor( 0, 0, 255, 220 ) );
pen.setStyle( Qt::PenStyle::DashLine );
painter->setPen( pen );
painter->drawLine( QPointF( rect.left() + left, rect.top() + blockBaseLine + fragmentVerticalOffset ),
QPointF( rect.left() + left, rect.top() + prevBlockBaseline ) );
}
painter->setPen( QColor( 0, 0, 255, 220 ) );
painter->drawLine( QPointF( rect.left() + left, rect.top() + blockBaseLine + fragmentVerticalOffset ),
QPointF( rect.left() + right, rect.top() + blockBaseLine + fragmentVerticalOffset ) );
left = right;
}
prevBlockBaseline = blockBaseLine;
} }
} }

View File

@ -23,6 +23,13 @@
#include <QFontMetricsF> #include <QFontMetricsF>
// to match QTextEngine handling of superscript/subscript font sizes
constexpr double SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR = 2.0 / 3.0;
// to match Qt behavior in QTextLine::draw
constexpr double SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR = 0.5;
constexpr double SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR = 1.0 / 6.0;
QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor ) QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor )
{ {
QgsTextDocumentMetrics res; QgsTextDocumentMetrics res;
@ -48,14 +55,25 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
QVector < double > blockVerticalLineSpacing; QVector < double > blockVerticalLineSpacing;
double outerXMin = 0;
double outerXMax = 0;
double outerYMinLabel = 0;
double outerYMaxLabel = 0;
for ( int blockIndex = 0; blockIndex < blockSize; blockIndex++ ) for ( int blockIndex = 0; blockIndex < blockSize; blockIndex++ )
{ {
const QgsTextBlock &block = document.at( blockIndex ); const QgsTextBlock &block = document.at( blockIndex );
double blockWidth = 0; double blockWidth = 0;
double blockXMax = 0;
double blockYMaxAdjustLabel = 0;
double blockHeightUsingAscentDescent = 0; double blockHeightUsingAscentDescent = 0;
double blockHeightUsingLineSpacing = 0; double blockHeightUsingLineSpacing = 0;
double blockHeightVerticalOrientation = 0; double blockHeightVerticalOrientation = 0;
double blockHeightUsingAscentAccountingForVerticalOffset = 0;
const int fragmentSize = block.size(); const int fragmentSize = block.size();
double maxBlockAscent = 0; double maxBlockAscent = 0;
@ -64,18 +82,89 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
double maxBlockLeading = 0; double maxBlockLeading = 0;
double maxBlockMaxWidth = 0; double maxBlockMaxWidth = 0;
QList< double > fragmentVerticalOffsets;
fragmentVerticalOffsets.reserve( fragmentSize );
QList< QFont > fragmentFonts; QList< QFont > fragmentFonts;
fragmentFonts.reserve( fragmentSize ); fragmentFonts.reserve( fragmentSize );
QList< double >fragmentHorizontalAdvance; QList< double >fragmentHorizontalAdvance;
fragmentHorizontalAdvance.reserve( fragmentSize ); fragmentHorizontalAdvance.reserve( fragmentSize );
QFont previousNonSuperSubScriptFont;
for ( int fragmentIndex = 0; fragmentIndex < fragmentSize; ++fragmentIndex ) for ( int fragmentIndex = 0; fragmentIndex < fragmentSize; ++fragmentIndex )
{ {
const QgsTextFragment &fragment = block.at( fragmentIndex ); const QgsTextFragment &fragment = block.at( fragmentIndex );
const QgsTextCharacterFormat &fragmentFormat = fragment.characterFormat(); const QgsTextCharacterFormat &fragmentFormat = fragment.characterFormat();
double fragmentHeightForVerticallyOffsetText = 0;
double fragmentYMaxAdjust = 0;
QFont updatedFont = font; QFont updatedFont = font;
fragmentFormat.updateFontForFormat( updatedFont, context, scaleFactor ); fragmentFormat.updateFontForFormat( updatedFont, context, scaleFactor );
const QFontMetricsF fm( updatedFont );
QFontMetricsF fm( updatedFont );
if ( fragmentIndex == 0 )
previousNonSuperSubScriptFont = updatedFont;
double fragmentVerticalOffset = 0;
if ( fragmentFormat.hasVerticalAlignmentSet() )
{
switch ( fragmentFormat.verticalAlignment() )
{
case Qgis::TextCharacterVerticalAlignment::Normal:
previousNonSuperSubScriptFont = updatedFont;
break;
case Qgis::TextCharacterVerticalAlignment::SuperScript:
{
const QFontMetricsF previousFM( previousNonSuperSubScriptFont );
if ( fragmentFormat.fontPointSize() < 0 )
{
// if fragment has no explicit font size set, then we scale the inherited font size to 60% of base font size
// this allows for easier use of super/subscript in labels as "my text<sup>2</sup>" will automatically render
// the superscript in a smaller font size. BUT if the fragment format HAS a non -1 font size then it indicates
// that the document has an explicit font size for the super/subscript element, eg "my text<sup style="font-size: 6pt">2</sup>"
// which we should respect
updatedFont.setPixelSize( updatedFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR );
fm = QFontMetricsF( updatedFont );
}
// to match Qt behavior in QTextLine::draw
fragmentVerticalOffset = -( previousFM.ascent() + previousFM.descent() ) * SUPERSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
// note -- this should really be fm.ascent(), not fm.capHeight() -- but in practice the ascent of most fonts is too large
// and causes unnecessarily large bounding boxes of vertically offset text!
fragmentHeightForVerticallyOffsetText = -fragmentVerticalOffset + fm.capHeight() / scaleFactor;
break;
}
case Qgis::TextCharacterVerticalAlignment::SubScript:
{
const QFontMetricsF previousFM( previousNonSuperSubScriptFont );
if ( fragmentFormat.fontPointSize() < 0 )
{
// see above!!
updatedFont.setPixelSize( updatedFont.pixelSize() * SUPERSCRIPT_SUBSCRIPT_FONT_SIZE_SCALING_FACTOR );
fm = QFontMetricsF( updatedFont );
}
// to match Qt behavior in QTextLine::draw
fragmentVerticalOffset = ( previousFM.ascent() + previousFM.descent() ) * SUBSCRIPT_VERTICAL_BASELINE_ADJUSTMENT_FACTOR / scaleFactor;
fragmentYMaxAdjust = fragmentVerticalOffset + fm.descent() / scaleFactor;
break;
}
}
}
else
{
previousNonSuperSubScriptFont = updatedFont;
}
fragmentVerticalOffsets << fragmentVerticalOffset;
const double fragmentWidth = fm.horizontalAdvance( fragment.text() ) / scaleFactor; const double fragmentWidth = fm.horizontalAdvance( fragment.text() ) / scaleFactor;
@ -85,12 +174,19 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
const double fragmentHeightUsingLineSpacing = fm.lineSpacing() / scaleFactor; const double fragmentHeightUsingLineSpacing = fm.lineSpacing() / scaleFactor;
blockWidth += fragmentWidth; blockWidth += fragmentWidth;
blockXMax += fragmentWidth;
blockHeightUsingAscentDescent = std::max( blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent ); blockHeightUsingAscentDescent = std::max( blockHeightUsingAscentDescent, fragmentHeightUsingAscentDescent );
blockHeightUsingLineSpacing = std::max( blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing ); blockHeightUsingLineSpacing = std::max( blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
maxBlockAscent = std::max( maxBlockAscent, fm.ascent() / scaleFactor ); maxBlockAscent = std::max( maxBlockAscent, fm.ascent() / scaleFactor );
blockHeightUsingAscentAccountingForVerticalOffset = std::max( std::max( maxBlockAscent, fragmentHeightForVerticallyOffsetText ), blockHeightUsingAscentAccountingForVerticalOffset );
maxBlockDescent = std::max( maxBlockDescent, fm.descent() / scaleFactor ); maxBlockDescent = std::max( maxBlockDescent, fm.descent() / scaleFactor );
maxBlockMaxWidth = std::max( maxBlockMaxWidth, fm.maxWidth() / scaleFactor ); maxBlockMaxWidth = std::max( maxBlockMaxWidth, fm.maxWidth() / scaleFactor );
blockYMaxAdjustLabel = std::max( blockYMaxAdjustLabel, fragmentYMaxAdjust );
if ( ( fm.lineSpacing() / scaleFactor ) > maxLineSpacing ) if ( ( fm.lineSpacing() / scaleFactor ) > maxLineSpacing )
{ {
maxLineSpacing = fm.lineSpacing() / scaleFactor; maxLineSpacing = fm.lineSpacing() / scaleFactor;
@ -117,6 +213,9 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
// for standard text rendering. Line height is also slightly different. // for standard text rendering. Line height is also slightly different.
currentLabelBaseline = -res.mFirstLineAscentOffset; currentLabelBaseline = -res.mFirstLineAscentOffset;
if ( blockHeightUsingAscentAccountingForVerticalOffset > maxBlockAscent )
outerYMinLabel = maxBlockAscent - blockHeightUsingAscentAccountingForVerticalOffset;
// standard rendering - designed to exactly replicate QPainter's drawText method // standard rendering - designed to exactly replicate QPainter's drawText method
currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/; currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/;
@ -141,11 +240,19 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
res.mLastLineAscentOffset = 0.25 * maxBlockAscent; res.mLastLineAscentOffset = 0.25 * maxBlockAscent;
} }
if ( blockIndex == blockSize - 1 )
{
if ( blockYMaxAdjustLabel > maxBlockDescent )
outerYMaxLabel = blockYMaxAdjustLabel - maxBlockDescent;
}
blockVerticalLineSpacing << ( format.lineHeightUnit() == QgsUnitTypes::RenderPercentage ? ( maxBlockMaxWidth * format.lineHeight() ) : lineHeightPainterUnits ); blockVerticalLineSpacing << ( format.lineHeightUnit() == QgsUnitTypes::RenderPercentage ? ( maxBlockMaxWidth * format.lineHeight() ) : lineHeightPainterUnits );
res.mBlockHeights << blockHeightUsingLineSpacing; res.mBlockHeights << blockHeightUsingLineSpacing;
width = std::max( width, blockWidth ); width = std::max( width, blockWidth );
outerXMax = std::max( outerXMax, blockXMax );
heightVerticalOrientation = std::max( heightVerticalOrientation, blockHeightVerticalOrientation ); heightVerticalOrientation = std::max( heightVerticalOrientation, blockHeightVerticalOrientation );
res.mBlockWidths << blockWidth; res.mBlockWidths << blockWidth;
res.mFragmentFonts << fragmentFonts; res.mFragmentFonts << fragmentFonts;
@ -154,6 +261,9 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
res.mBaselineOffsetsRectMode << currentRectBaseline; res.mBaselineOffsetsRectMode << currentRectBaseline;
res.mBlockMaxDescent << maxBlockDescent; res.mBlockMaxDescent << maxBlockDescent;
res.mBlockMaxCharacterWidth << maxBlockMaxWidth; res.mBlockMaxCharacterWidth << maxBlockMaxWidth;
res.mFragmentVerticalOffsetsLabelMode << fragmentVerticalOffsets;
res.mFragmentVerticalOffsetsRectMode << fragmentVerticalOffsets;
res.mFragmentVerticalOffsetsPointMode << fragmentVerticalOffsets;
res.mFragmentHorizontalAdvance << fragmentHorizontalAdvance; res.mFragmentHorizontalAdvance << fragmentHorizontalAdvance;
if ( blockIndex > 0 ) if ( blockIndex > 0 )
@ -169,6 +279,8 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
// adjust baselines // adjust baselines
if ( !res.mBaselineOffsetsLabelMode.isEmpty() ) if ( !res.mBaselineOffsetsLabelMode.isEmpty() )
{ {
// outerYMinLabel += res.mBaselineOffsetsLabelMode[0];
const double labelModeBaselineAdjust = res.mBaselineOffsetsLabelMode.constLast() + res.mLastLineAscentOffset; const double labelModeBaselineAdjust = res.mBaselineOffsetsLabelMode.constLast() + res.mLastLineAscentOffset;
const double pointModeBaselineAdjust = res.mBaselineOffsetsPointMode.constLast(); const double pointModeBaselineAdjust = res.mBaselineOffsetsPointMode.constLast();
for ( int i = 0; i < blockSize; ++i ) for ( int i = 0; i < blockSize; ++i )
@ -204,6 +316,10 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
res.mDocumentSizeVerticalOrientation = QSizeF( widthVerticalOrientation, heightVerticalOrientation ); res.mDocumentSizeVerticalOrientation = QSizeF( widthVerticalOrientation, heightVerticalOrientation );
} }
res.mOuterBoundsLabelMode = QRectF( outerXMin, -outerYMaxLabel,
outerXMax - outerXMin,
heightLabelMode - outerYMinLabel + outerYMaxLabel );
return res; return res;
} }
@ -232,6 +348,31 @@ QSizeF QgsTextDocumentMetrics::documentSize( Qgis::TextLayoutMode mode, Qgis::Te
BUILTIN_UNREACHABLE BUILTIN_UNREACHABLE
} }
QRectF QgsTextDocumentMetrics::outerBounds( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const
{
switch ( orientation )
{
case Qgis::TextOrientation::Horizontal:
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::Point:
return QRectF();
case Qgis::TextLayoutMode::Labeling:
return mOuterBoundsLabelMode;
};
BUILTIN_UNREACHABLE
case Qgis::TextOrientation::Vertical:
return QRectF();
case Qgis::TextOrientation::RotationBased:
return QRectF(); // label mode only
}
BUILTIN_UNREACHABLE
}
double QgsTextDocumentMetrics::blockWidth( int blockIndex ) const double QgsTextDocumentMetrics::blockWidth( int blockIndex ) const
{ {
return mBlockWidths.value( blockIndex ); return mBlockWidths.value( blockIndex );
@ -261,6 +402,20 @@ double QgsTextDocumentMetrics::fragmentHorizontalAdvance( int blockIndex, int fr
return mFragmentHorizontalAdvance.value( blockIndex ).value( fragmentIndex ); return mFragmentHorizontalAdvance.value( blockIndex ).value( fragmentIndex );
} }
double QgsTextDocumentMetrics::fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const
{
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
return mFragmentVerticalOffsetsRectMode.value( blockIndex ).value( fragmentIndex );
case Qgis::TextLayoutMode::Point:
return mFragmentVerticalOffsetsPointMode.value( blockIndex ).value( fragmentIndex );
case Qgis::TextLayoutMode::Labeling:
return mFragmentVerticalOffsetsLabelMode.value( blockIndex ).value( fragmentIndex );
}
BUILTIN_UNREACHABLE
}
double QgsTextDocumentMetrics::verticalOrientationXOffset( int blockIndex ) const double QgsTextDocumentMetrics::verticalOrientationXOffset( int blockIndex ) const
{ {
return mVerticalOrientationXOffsets.value( blockIndex ); return mVerticalOrientationXOffsets.value( blockIndex );

View File

@ -22,6 +22,7 @@
#include <QVector> #include <QVector>
#include <QSizeF> #include <QSizeF>
#include <QRectF>
class QgsTextDocument; class QgsTextDocument;
class QgsRenderContext; class QgsRenderContext;
@ -64,6 +65,17 @@ class CORE_EXPORT QgsTextDocumentMetrics
*/ */
QSizeF documentSize( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const; QSizeF documentSize( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const;
/**
* Returns the outer bounds of the document, which is the documentSize() adjusted to account
* for any text elements which fall outside of the usual document margins (such as super or
* sub script elements)
*
* \warning Currently this is only supported for the Qgis::TextLayoutMode::Labeling mode.
*
* \since QGIS 3.30
*/
QRectF outerBounds( Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation ) const;
/** /**
* Returns the width of the block at the specified index. * Returns the width of the block at the specified index.
*/ */
@ -86,6 +98,14 @@ class CORE_EXPORT QgsTextDocumentMetrics
*/ */
double fragmentHorizontalAdvance( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const; double fragmentHorizontalAdvance( 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 the specified index within that block.
*
* \since QGIS 3.30
*/
double fragmentVerticalOffset( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
/** /**
* Returns the vertical orientation x offset for the specified block. * Returns the vertical orientation x offset for the specified block.
*/ */
@ -119,6 +139,8 @@ class CORE_EXPORT QgsTextDocumentMetrics
QSizeF mDocumentSizePointRectMode; QSizeF mDocumentSizePointRectMode;
QSizeF mDocumentSizeVerticalOrientation; QSizeF mDocumentSizeVerticalOrientation;
QRectF mOuterBoundsLabelMode;
QList < QList< QFont > > mFragmentFonts; QList < QList< QFont > > mFragmentFonts;
QList< double > mBlockWidths; QList< double > mBlockWidths;
QList< double > mBlockHeights; QList< double > mBlockHeights;
@ -128,6 +150,10 @@ class CORE_EXPORT QgsTextDocumentMetrics
QList< QList< double > > mFragmentHorizontalAdvance; QList< QList< double > > mFragmentHorizontalAdvance;
QList< QList< double > > mFragmentVerticalOffsetsLabelMode;
QList< QList< double > > mFragmentVerticalOffsetsPointMode;
QList< QList< double > > mFragmentVerticalOffsetsRectMode;
QList< double > mVerticalOrientationXOffsets; QList< double > mVerticalOrientationXOffsets;
QList< double > mBlockMaxDescent; QList< double > mBlockMaxDescent;
QList< double > mBlockMaxCharacterWidth; QList< double > mBlockMaxCharacterWidth;

View File

@ -364,7 +364,8 @@ double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRend
if ( component.extraWordSpacing || component.extraLetterSpacing ) if ( component.extraWordSpacing || component.extraLetterSpacing )
applyExtraSpacingForLineJustification( fragmentFont, component.extraWordSpacing, component.extraLetterSpacing ); applyExtraSpacingForLineJustification( fragmentFont, component.extraWordSpacing, component.extraLetterSpacing );
path.addText( xOffset, 0, fragmentFont, fragment.text() ); const double yOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ); xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode );
@ -390,12 +391,15 @@ double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRend
const QFontMetricsF fragmentMetrics( fragmentFont ); const QFontMetricsF fragmentMetrics( fragmentFont );
const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode )
/ 1;
const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() ); const QStringList parts = QgsPalLabeling::splitToGraphemes( fragment.text() );
for ( const QString &part : parts ) for ( const QString &part : parts )
{ {
double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2; double partXOffset = ( blockMaximumCharacterWidth - ( fragmentMetrics.horizontalAdvance( part ) / scaleFactor - letterSpacing ) ) / 2;
partYOffset += fragmentMetrics.ascent() / scaleFactor; partYOffset += fragmentMetrics.ascent() / scaleFactor;
path.addText( partXOffset, partYOffset, fragmentFont, part ); path.addText( partXOffset, partYOffset + fragmentYOffset, fragmentFont, part );
partYOffset += letterSpacing; partYOffset += letterSpacing;
} }
partLastDescent = fragmentMetrics.descent() / scaleFactor; partLastDescent = fragmentMetrics.descent() / scaleFactor;
@ -525,7 +529,8 @@ void QgsTextRenderer::drawMask( QgsRenderContext &context, const QgsTextRenderer
{ {
const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex ); const QFont fragmentFont = metrics.fragmentFont( component.blockIndex, fragmentIndex );
path.addText( xOffset, 0, fragmentFont, fragment.text() ); const double fragmentYOffset = metrics.fragmentVerticalOffset( component.blockIndex, fragmentIndex, mode );
path.addText( xOffset, fragmentYOffset, fragmentFont, fragment.text() );
xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode ); xOffset += metrics.fragmentHorizontalAdvance( component.blockIndex, fragmentIndex, mode );
fragmentIndex++; fragmentIndex++;
@ -1621,7 +1626,9 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
if ( extraWordSpace || extraLetterSpace ) if ( extraWordSpace || extraLetterSpace )
applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale ); applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
path.addText( xOffset, 0, fragmentFont, fragment.text() ); const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
path.addText( xOffset, yOffset, fragmentFont, fragment.text() );
QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color(); QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() ); textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
@ -1673,6 +1680,8 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
if ( extraWordSpace || extraLetterSpace ) if ( extraWordSpace || extraLetterSpace )
applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale ); applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );
const double yOffset = metrics.fragmentVerticalOffset( blockIndex, fragmentIndex, mode );
QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color(); QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() ); textColor.setAlphaF( fragment.characterFormat().textColor().isValid() ? textColor.alphaF() * format.opacity() : format.opacity() );
@ -1681,7 +1690,7 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
context.painter()->setRenderHint( QPainter::TextAntialiasing ); context.painter()->setRenderHint( QPainter::TextAntialiasing );
context.painter()->scale( 1 / fontScale, 1 / fontScale ); context.painter()->scale( 1 / fontScale, 1 / fontScale );
context.painter()->drawText( xOffset, 0, fragment.text() ); context.painter()->drawText( xOffset, yOffset, fragment.text() );
context.painter()->scale( fontScale, fontScale ); context.painter()->scale( fontScale, fontScale );
xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode ); xOffset += metrics.fragmentHorizontalAdvance( blockIndex, fragmentIndex, mode );

View File

@ -3310,6 +3310,75 @@ class PyQgsTextRenderer(unittest.TestCase):
'<i>t</i><b style="font-size: 30pt">e</b><p><span style="color: red">s<span style="color: rgba(255,0,0,0.5); text-decoration: underline; font-size:80pt">t</span></span>'], '<i>t</i><b style="font-size: 30pt">e</b><p><span style="color: red">s<span style="color: rgba(255,0,0,0.5); text-decoration: underline; font-size:80pt">t</span></span>'],
point=QPointF(50, 200)) point=QPointF(50, 200))
def testHtmlSuperSubscript(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(60)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
format.setColor(QColor(255, 0, 0))
format.setAllowHtmlFormatting(True)
assert self.checkRenderPoint(format, 'text_html_supersubscript', None, text=[
'<sub>sub</sub>N<sup>sup</sup>'],
point=QPointF(50, 200))
def testHtmlSuperSubscriptFixedFontSize(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(60)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
format.setColor(QColor(255, 0, 0))
format.setAllowHtmlFormatting(True)
assert self.checkRenderPoint(format, 'text_html_supersubscript_fixed_font_size', None, text=[
'<sub style="font-size:80pt">s<span style="font-size:30pt">u</span></sub>N<sup style="font-size:40pt">s<span style="font-size: 20pt">up</span></sup>'],
point=QPointF(50, 200))
def testHtmlSuperSubscriptBuffer(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(60)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
format.setColor(QColor(255, 0, 0))
format.setAllowHtmlFormatting(True)
format.buffer().setEnabled(True)
format.buffer().setSize(5)
format.buffer().setColor(QColor(50, 150, 200))
assert self.checkRenderPoint(format, 'text_html_supersubscript_buffer', None, text=[
'<sub>sub</sub>N<sup>sup</sup>'],
point=QPointF(50, 200))
def testHtmlSuperSubscriptShadow(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(60)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
format.setColor(QColor(0, 255, 0))
format.setAllowHtmlFormatting(True)
format.shadow().setEnabled(True)
format.shadow().setOffsetDistance(5)
format.shadow().setBlurRadius(0)
format.shadow().setColor(QColor(50, 150, 200))
assert self.checkRenderPoint(format, 'text_html_supersubscript_shadow', None, text=[
'<sub>sub</sub>N<sup>sup</sup>'],
point=QPointF(50, 200))
def testHtmlSuperSubscriptBufferShadow(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(60)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
format.setColor(QColor(0, 255, 0))
format.setAllowHtmlFormatting(True)
format.buffer().setEnabled(True)
format.buffer().setSize(5)
format.buffer().setColor(QColor(200, 50, 150))
format.shadow().setEnabled(True)
format.shadow().setOffsetDistance(5)
format.shadow().setBlurRadius(0)
format.shadow().setColor(QColor(50, 150, 200))
assert self.checkRenderPoint(format, 'text_html_supersubscript_buffer_shadow', None, text=[
'<sub>sub</sub>N<sup>sup</sup>'],
point=QPointF(50, 200))
def testTextRenderFormat(self): def testTextRenderFormat(self):
format = QgsTextFormat() format = QgsTextFormat()
format.setFont(getTestFont('bold')) format.setFont(getTestFont('bold'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB