Skip to content

Commit

Permalink
Add additional text renderer rect modes
Browse files Browse the repository at this point in the history
Adds some text layout modes where the line heights are based
on cap heights or font ascent alone

These are required in order to port additional parts of QGIS
text rendering to the QgsTextRenderer class
  • Loading branch information
nyalldawson committed Nov 24, 2022
1 parent 12ec85a commit a10b971
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 5 deletions.
8 changes: 7 additions & 1 deletion python/core/auto_additions/qgis.py
Expand Up @@ -1479,7 +1479,13 @@
QgsTextRenderer.Label = Qgis.TextLayoutMode.Labeling
QgsTextRenderer.Label.is_monkey_patched = True
QgsTextRenderer.Label.__doc__ = "Labeling-specific layout mode"
Qgis.TextLayoutMode.__doc__ = 'Text layout modes.\n\n.. note::\n\n Prior to QGIS 3.28 this was available as :py:class:`QgsTextRenderer`.DrawMode\n\n.. versionadded:: 3.28\n\n' + '* ``Rect``: ' + Qgis.TextLayoutMode.Rectangle.__doc__ + '\n' + '* ``Point``: ' + Qgis.TextLayoutMode.Point.__doc__ + '\n' + '* ``Label``: ' + Qgis.TextLayoutMode.Labeling.__doc__
QgsTextRenderer.RectangleCapHeightBased = Qgis.TextLayoutMode.RectangleCapHeightBased
QgsTextRenderer.RectangleCapHeightBased.is_monkey_patched = True
QgsTextRenderer.RectangleCapHeightBased.__doc__ = "Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line of text, and cap height + descent for subsequent lines of text (since QGIS 3.30)"
QgsTextRenderer.RectangleAscentBased = Qgis.TextLayoutMode.RectangleAscentBased
QgsTextRenderer.RectangleAscentBased.is_monkey_patched = True
QgsTextRenderer.RectangleAscentBased.__doc__ = "Similar to Rectangle mode, but uses ascents only when calculating font and line heights. (since QGIS 3.30)"
Qgis.TextLayoutMode.__doc__ = 'Text layout modes.\n\n.. note::\n\n Prior to QGIS 3.28 this was available as :py:class:`QgsTextRenderer`.DrawMode\n\n.. versionadded:: 3.28\n\n' + '* ``Rect``: ' + Qgis.TextLayoutMode.Rectangle.__doc__ + '\n' + '* ``Point``: ' + Qgis.TextLayoutMode.Point.__doc__ + '\n' + '* ``Label``: ' + Qgis.TextLayoutMode.Labeling.__doc__ + '\n' + '* ``RectangleCapHeightBased``: ' + Qgis.TextLayoutMode.RectangleCapHeightBased.__doc__ + '\n' + '* ``RectangleAscentBased``: ' + Qgis.TextLayoutMode.RectangleAscentBased.__doc__
# --
Qgis.TextLayoutMode.baseClass = Qgis
QgsTextRenderer.TextPart = Qgis.TextComponent
Expand Down
2 changes: 2 additions & 0 deletions python/core/auto_generated/qgis.sip.in
Expand Up @@ -982,6 +982,8 @@ The development version
Rectangle,
Point,
Labeling,
RectangleCapHeightBased,
RectangleAscentBased,
};

enum class TextComponent
Expand Down
Expand Up @@ -72,6 +72,13 @@ Returns the width of the block at the specified index.
double blockHeight( int blockIndex ) const;
%Docstring
Returns the height of the block at the specified index.
%End

double firstLineCapHeight() const;
%Docstring
Returns the cap height for the first line of text.

.. versionadded:: 3.30
%End

double baselineOffset( int blockIndex, Qgis::TextLayoutMode mode ) const;
Expand Down
Expand Up @@ -59,7 +59,8 @@ Calculates pixel size (considering output size should be in pixel or map units,
static void drawText( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines,
QgsRenderContext &context, const QgsTextFormat &format,
bool drawAsOutlines = true, Qgis::TextVerticalAlignment vAlignment = Qgis::TextVerticalAlignment::Top,
Qgis::TextRendererFlags flags = Qgis::TextRendererFlags() );
Qgis::TextRendererFlags flags = Qgis::TextRendererFlags(),
Qgis::TextLayoutMode mode = Qgis::TextLayoutMode::Rectangle );
%Docstring
Draws text within a rectangle using the specified settings.

Expand All @@ -75,6 +76,7 @@ Draws text within a rectangle using the specified settings.
as of QGIS 3.4.3 and the text format should be set using :py:func:`QgsRenderContext.setTextRenderFormat()` instead.
:param vAlignment: vertical alignment (since QGIS 3.16)
:param flags: text rendering flags (since QGIS 3.24)
:param mode: text layout mode. Only Qgis.TextLayoutMode.Rectangle, Qgis.TextLayoutMode.RectangleCapHeightBased and Qgis.TextLayoutMode.RectangleAscentBased are accepted (since QGIS 3.30)
%End

static void drawText( QPointF point, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines,
Expand Down
2 changes: 2 additions & 0 deletions src/core/qgis.h
Expand Up @@ -1632,6 +1632,8 @@ class CORE_EXPORT Qgis
Rectangle SIP_MONKEYPATCH_COMPAT_NAME( Rect ), //!< Text within rectangle layout mode
Point, //!< Text at point of origin layout mode
Labeling SIP_MONKEYPATCH_COMPAT_NAME( Label ), //!< Labeling-specific layout mode
RectangleCapHeightBased, //!< Similar to Rectangle mode, but uses cap height only when calculating font heights for the first line of text, and cap height + descent for subsequent lines of text (since QGIS 3.30)
RectangleAscentBased, //!< Similar to Rectangle mode, but uses ascents only when calculating font and line heights. (since QGIS 3.30)
};
Q_ENUM( TextLayoutMode )

Expand Down
42 changes: 42 additions & 0 deletions src/core/textrenderer/qgstextdocumentmetrics.cpp
Expand Up @@ -44,11 +44,15 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
double width = 0;
double heightLabelMode = 0;
double heightPointRectMode = 0;
double heightCapHeightMode = 0;
double heightAscentMode = 0;
const int blockSize = document.size();
res.mFragmentFonts.reserve( blockSize );
double currentLabelBaseline = 0;
double currentPointBaseline = 0;
double currentRectBaseline = 0;
double currentCapHeightBasedBaseline = 0;
double currentAscentBasedBaseline = 0;
double lastLineLeading = 0;

double heightVerticalOrientation = 0;
Expand Down Expand Up @@ -81,6 +85,7 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
double maxLineSpacing = 0;
double maxBlockLeading = 0;
double maxBlockMaxWidth = 0;
double maxBlockCapHeight = 0;

QList< double > fragmentVerticalOffsets;
fragmentVerticalOffsets.reserve( fragmentSize );
Expand Down Expand Up @@ -180,6 +185,8 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
blockHeightUsingLineSpacing = std::max( blockHeightUsingLineSpacing, fragmentHeightUsingLineSpacing );
maxBlockAscent = std::max( maxBlockAscent, fm.ascent() / scaleFactor );

maxBlockCapHeight = std::max( maxBlockCapHeight, fm.capHeight() / scaleFactor );

blockHeightUsingAscentAccountingForVerticalOffset = std::max( std::max( maxBlockAscent, fragmentHeightForVerticallyOffsetText ), blockHeightUsingAscentAccountingForVerticalOffset );

maxBlockDescent = std::max( maxBlockDescent, fm.descent() / scaleFactor );
Expand All @@ -206,6 +213,7 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
// needed to move bottom of text's descender to within bottom edge of label
res.mFirstLineAscentOffset = 0.25 * maxBlockAscent; // descent() is not enough
res.mLastLineAscentOffset = res.mFirstLineAscentOffset;
res.mFirstLineCapHeight = maxBlockCapHeight;
const double lineHeight = ( maxBlockAscent + maxBlockDescent ); // ignore +1 for baseline

// rendering labels needs special handling - in this case text should be
Expand All @@ -219,11 +227,16 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
// standard rendering - designed to exactly replicate QPainter's drawText method
currentRectBaseline = -res.mFirstLineAscentOffset + lineHeight - 1 /*baseline*/;

currentCapHeightBasedBaseline = res.mFirstLineCapHeight;
currentAscentBasedBaseline = maxBlockAscent;

// standard rendering - designed to exactly replicate QPainter's drawText rect method
currentPointBaseline = 0;

heightLabelMode += blockHeightUsingAscentDescent;
heightPointRectMode += blockHeightUsingAscentDescent;
heightCapHeightMode += maxBlockCapHeight;
heightAscentMode += maxBlockAscent;
}
else
{
Expand All @@ -233,9 +246,15 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
currentLabelBaseline += thisLineHeightUsingAscentDescent;
currentRectBaseline += thisLineHeightUsingLineSpacing;
currentPointBaseline += thisLineHeightUsingLineSpacing;
// using cap height??
currentCapHeightBasedBaseline += thisLineHeightUsingLineSpacing;
// using ascent?
currentAscentBasedBaseline += thisLineHeightUsingLineSpacing;

heightLabelMode += thisLineHeightUsingAscentDescent;
heightPointRectMode += thisLineHeightUsingLineSpacing;
heightCapHeightMode += thisLineHeightUsingLineSpacing;
heightAscentMode += thisLineHeightUsingLineSpacing;
if ( blockIndex == blockSize - 1 )
res.mLastLineAscentOffset = 0.25 * maxBlockAscent;
}
Expand All @@ -259,6 +278,8 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo
res.mBaselineOffsetsLabelMode << currentLabelBaseline;
res.mBaselineOffsetsPointMode << currentPointBaseline;
res.mBaselineOffsetsRectMode << currentRectBaseline;
res.mBaselineOffsetsCapHeightMode << currentCapHeightBasedBaseline;
res.mBaselineOffsetsAscentBased << currentAscentBasedBaseline;
res.mBlockMaxDescent << maxBlockDescent;
res.mBlockMaxCharacterWidth << maxBlockMaxWidth;
res.mFragmentVerticalOffsetsLabelMode << fragmentVerticalOffsets;
Expand All @@ -275,6 +296,8 @@ QgsTextDocumentMetrics QgsTextDocumentMetrics::calculateMetrics( const QgsTextDo

res.mDocumentSizeLabelMode = QSizeF( width, heightLabelMode );
res.mDocumentSizePointRectMode = QSizeF( width, heightPointRectMode );
res.mDocumentSizeCapHeightMode = QSizeF( width, heightCapHeightMode );
res.mDocumentSizeAscentMode = QSizeF( width, heightAscentMode );

// adjust baselines
if ( !res.mBaselineOffsetsLabelMode.isEmpty() )
Expand Down Expand Up @@ -332,6 +355,12 @@ QSizeF QgsTextDocumentMetrics::documentSize( Qgis::TextLayoutMode mode, Qgis::Te
case Qgis::TextLayoutMode::Point:
return mDocumentSizePointRectMode;

case Qgis::TextLayoutMode::RectangleCapHeightBased:
return mDocumentSizeCapHeightMode;

case Qgis::TextLayoutMode::RectangleAscentBased:
return mDocumentSizeAscentMode;

case Qgis::TextLayoutMode::Labeling:
return mDocumentSizeLabelMode;
};
Expand All @@ -354,6 +383,8 @@ QRectF QgsTextDocumentMetrics::outerBounds( Qgis::TextLayoutMode mode, Qgis::Tex
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
case Qgis::TextLayoutMode::Point:
return QRectF();

Expand All @@ -380,12 +411,21 @@ double QgsTextDocumentMetrics::blockHeight( int blockIndex ) const
return mBlockHeights.value( blockIndex );
}

double QgsTextDocumentMetrics::firstLineCapHeight() const
{
return mFirstLineCapHeight;
}

double QgsTextDocumentMetrics::baselineOffset( int blockIndex, Qgis::TextLayoutMode mode ) const
{
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
return mBaselineOffsetsRectMode.value( blockIndex );
case Qgis::TextLayoutMode::RectangleCapHeightBased:
return mBaselineOffsetsCapHeightMode.value( blockIndex );
case Qgis::TextLayoutMode::RectangleAscentBased:
return mBaselineOffsetsAscentBased.value( blockIndex );
case Qgis::TextLayoutMode::Point:
return mBaselineOffsetsPointMode.value( blockIndex );
case Qgis::TextLayoutMode::Labeling:
Expand All @@ -404,6 +444,8 @@ double QgsTextDocumentMetrics::fragmentVerticalOffset( int blockIndex, int fragm
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
return mFragmentVerticalOffsetsRectMode.value( blockIndex ).value( fragmentIndex );
case Qgis::TextLayoutMode::Point:
return mFragmentVerticalOffsetsPointMode.value( blockIndex ).value( fragmentIndex );
Expand Down
12 changes: 12 additions & 0 deletions src/core/textrenderer/qgstextdocumentmetrics.h
Expand Up @@ -86,6 +86,13 @@ class CORE_EXPORT QgsTextDocumentMetrics
*/
double blockHeight( int blockIndex ) const;

/**
* Returns the cap height for the first line of text.
*
* \since QGIS 3.30
*/
double firstLineCapHeight() const;

/**
* Returns the offset from the top of the document to the text baseline for the given block index.
*/
Expand Down Expand Up @@ -138,6 +145,8 @@ class CORE_EXPORT QgsTextDocumentMetrics
QSizeF mDocumentSizeLabelMode;
QSizeF mDocumentSizePointRectMode;
QSizeF mDocumentSizeVerticalOrientation;
QSizeF mDocumentSizeCapHeightMode;
QSizeF mDocumentSizeAscentMode;

QRectF mOuterBoundsLabelMode;

Expand All @@ -147,6 +156,8 @@ class CORE_EXPORT QgsTextDocumentMetrics
QList< double > mBaselineOffsetsLabelMode;
QList< double > mBaselineOffsetsPointMode;
QList< double > mBaselineOffsetsRectMode;
QList< double > mBaselineOffsetsCapHeightMode;
QList< double > mBaselineOffsetsAscentBased;

QList< QList< double > > mFragmentHorizontalAdvance;

Expand All @@ -159,6 +170,7 @@ class CORE_EXPORT QgsTextDocumentMetrics
QList< double > mBlockMaxCharacterWidth;
double mFirstLineAscentOffset = 0;
double mLastLineAscentOffset = 0;
double mFirstLineCapHeight = 0;

};

Expand Down
15 changes: 14 additions & 1 deletion src/core/textrenderer/qgstextrenderer.cpp
Expand Up @@ -78,7 +78,8 @@ int QgsTextRenderer::sizeToPixel( double size, const QgsRenderContext &c, QgsUni
return static_cast< int >( c.convertToPainterUnits( size, unit, mapUnitScale ) + 0.5 ); //NOLINT
}

void QgsTextRenderer::drawText( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &text, QgsRenderContext &context, const QgsTextFormat &format, bool, Qgis::TextVerticalAlignment vAlignment, Qgis::TextRendererFlags flags )
void QgsTextRenderer::drawText( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &text, QgsRenderContext &context, const QgsTextFormat &format, bool, Qgis::TextVerticalAlignment vAlignment, Qgis::TextRendererFlags flags,
Qgis::TextLayoutMode mode )
{
QgsTextFormat tmpFormat = format;
if ( format.dataDefinedProperties().hasActiveProperties() ) // note, we use format instead of tmpFormat here, it's const and potentially avoids a detach
Expand Down Expand Up @@ -812,6 +813,8 @@ void QgsTextRenderer::drawBackground( QgsRenderContext &context, QgsTextRenderer
switch ( mode )
{
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
switch ( component.hAlign )
{
case Qgis::TextHorizontalAlignment::Left:
Expand Down Expand Up @@ -1458,6 +1461,8 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
break;

case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
labelWidest = component.size.width();
break;
}
Expand Down Expand Up @@ -1541,6 +1546,8 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
{
case Qgis::TextLayoutMode::Labeling:
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
xMultiLineOffset = labelWidthDiff;
break;

Expand Down Expand Up @@ -1736,6 +1743,8 @@ void QgsTextRenderer::drawTextInternalVertical( QgsRenderContext &context, const
break;

case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
textRectWidth = component.size.width();
break;
}
Expand Down Expand Up @@ -1795,6 +1804,8 @@ void QgsTextRenderer::drawTextInternalVertical( QgsRenderContext &context, const
{
case Qgis::TextLayoutMode::Labeling:
case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
xOffset += hAlignmentOffset;
break;

Expand Down Expand Up @@ -1830,6 +1841,8 @@ void QgsTextRenderer::drawTextInternalVertical( QgsRenderContext &context, const
break;

case Qgis::TextLayoutMode::Rectangle:
case Qgis::TextLayoutMode::RectangleCapHeightBased:
case Qgis::TextLayoutMode::RectangleAscentBased:
yOffset = 0;
break;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/textrenderer/qgstextrenderer.h
Expand Up @@ -85,6 +85,7 @@ class CORE_EXPORT QgsTextRenderer
* as of QGIS 3.4.3 and the text format should be set using QgsRenderContext::setTextRenderFormat() instead.
* \param vAlignment vertical alignment (since QGIS 3.16)
* \param flags text rendering flags (since QGIS 3.24)
* \param mode text layout mode. Only Qgis::TextLayoutMode::Rectangle, Qgis::TextLayoutMode::RectangleCapHeightBased and Qgis::TextLayoutMode::RectangleAscentBased are accepted (since QGIS 3.30)
*/
static void drawText( const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines,
QgsRenderContext &context, const QgsTextFormat &format,
Expand Down
46 changes: 44 additions & 2 deletions tests/src/python/test_qgstextrenderer.py
Expand Up @@ -1480,7 +1480,8 @@ def checkRender(self, format, name, part=None, angle=0, alignment=QgsTextRendere
rect=QRectF(100, 100, 50, 250),
vAlignment=QgsTextRenderer.AlignTop,
flags=Qgis.TextRendererFlags(),
image_size=400):
image_size=400,
mode=Qgis.TextLayoutMode.Rectangle):

image = QImage(image_size, image_size, QImage.Format_RGB32)

Expand Down Expand Up @@ -1516,7 +1517,10 @@ def checkRender(self, format, name, part=None, angle=0, alignment=QgsTextRendere
alignment,
text,
context,
format, vAlignment=vAlignment, flags=flags)
format,
vAlignment=vAlignment,
flags=flags,
mode=mode)

painter.setFont(format.scaledFont(context))
painter.setPen(QPen(QColor(255, 0, 255, 200)))
Expand Down Expand Up @@ -1601,6 +1605,44 @@ def testDrawRectMixedHtml(self):
format.setSize(30)
assert self.checkRender(format, 'rect_html', rect=QRectF(100, 100, 100, 100), text=['first <span style="font-size:50pt">line</span>', 'second <span style="font-size:50pt">line</span>', 'third line'])

def testDrawRectCapHeightMode(self):
"""
Test drawing text in rect mode with cap height based line heights
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(30)
assert self.checkRender(format, 'rect_cap_height_mode', rect=QRectF(100, 100, 100, 100), text=['first line', 'second line', 'third line'], mode=Qgis.TextLayoutMode.RectangleCapHeightBased)

def testDrawRectCapHeightModeMixedHtml(self):
"""
Test drawing text in rect mode with cap height based line heights
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setAllowHtmlFormatting(True)
format.setSize(30)
assert self.checkRender(format, 'rect_cap_height_mode_html', rect=QRectF(100, 100, 100, 100), text=['first <span style="font-size:50pt">line</span>', 'second <span style="font-size:50pt">line</span>', 'third line'], mode=Qgis.TextLayoutMode.RectangleCapHeightBased)

def testDrawRectAscentMode(self):
"""
Test drawing text in rect mode with cap height based line heights
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(30)
assert self.checkRender(format, 'rect_ascent_mode', rect=QRectF(100, 100, 100, 100), text=['first line', 'second line', 'third line'], mode=Qgis.TextLayoutMode.RectangleAscentBased)

def testDrawRectAscentModeMixedHtml(self):
"""
Test drawing text in rect mode with ascent based line heights
"""
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setAllowHtmlFormatting(True)
format.setSize(30)
assert self.checkRender(format, 'rect_ascent_mode_html', rect=QRectF(100, 100, 100, 100), text=['first <span style="font-size:50pt">line</span>', 'second <span style="font-size:50pt">line</span>', 'third line'], mode=Qgis.TextLayoutMode.RectangleAscentBased)

def testDrawForcedItalic(self):
"""
Test drawing with forced italic
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a10b971

Please sign in to comment.