Skip to content

Commit

Permalink
Add "justify" alignment mode for QgsTextRenderer
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Jul 9, 2020
1 parent 52f7741 commit 445beba
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 10 deletions.
76 changes: 67 additions & 9 deletions src/core/textrenderer/qgstextrenderer.cpp
Expand Up @@ -302,6 +302,9 @@ double QgsTextRenderer::drawBuffer( QgsRenderContext &context, const QgsTextRend
QFont fragmentFont = font;
fragment.characterFormat().updateFontForFormat( fragmentFont, scaleFactor );

if ( component.extraWordSpacing || component.extraLetterSpacing )
applyExtraSpacingForLineJustification( fragmentFont, component.extraWordSpacing, component.extraLetterSpacing );

path.addText( xOffset, 0, fragmentFont, fragment.text() );

xOffset += fragment.horizontalAdvance( fragmentFont, true, scaleFactor );
Expand Down Expand Up @@ -1278,6 +1281,51 @@ QgsTextFormat::TextOrientation QgsTextRenderer::calculateRotationAndOrientationF
return QgsTextFormat::HorizontalOrientation;
}

void QgsTextRenderer::calculateExtraSpacingForLineJustification( const double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace )
{
const QString blockText = block.toPlainText();
QTextBoundaryFinder finder( QTextBoundaryFinder::Word, blockText );
finder.toStart();
int wordBoundaries = 0;
while ( finder.toNextBoundary() != -1 )
{
if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
wordBoundaries++;
}

if ( wordBoundaries > 0 )
{
// word boundaries found => justify by padding word spacing
extraWordSpace = spaceToDistribute / wordBoundaries;
}
else
{
// no word boundaries found => justify by letter spacing
QTextBoundaryFinder finder( QTextBoundaryFinder::Grapheme, blockText );
finder.toStart();

int graphemeBoundaries = 0;
while ( finder.toNextBoundary() != -1 )
{
if ( finder.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
graphemeBoundaries++;
}

if ( graphemeBoundaries > 0 )
{
extraLetterSpace = spaceToDistribute / graphemeBoundaries;
}
}
}

void QgsTextRenderer::applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace )
{
const double prevWordSpace = font.wordSpacing();
font.setWordSpacing( prevWordSpace + extraWordSpace );
const double prevLetterSpace = font.letterSpacing();
font.setLetterSpacing( QFont::AbsoluteSpacing, prevLetterSpace + extraLetterSpace );
}

void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, const QgsTextFormat &format, TextPart drawType, DrawMode mode, const Component &component, const QgsTextDocument &document, double fontScale, const QFontMetricsF *fontMetrics, HAlignment hAlignment,
VAlignment vAlignment, double rotation )
{
Expand Down Expand Up @@ -1337,6 +1385,8 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
{
const QgsTextBlock block = document.at( i );

const bool isFinalLine = i == document.size() - 1;

QgsScopedQPainterState painterState( context.painter() );
context.setPainterFlagsUsingContext();
context.painter()->translate( component.origin );
Expand All @@ -1355,6 +1405,8 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
// figure x offset for horizontal alignment of multiple lines
double xMultiLineOffset = 0.0;
double labelWidth = fontMetrics->width( line ) / fontScale;
double extraWordSpace = 0;
double extraLetterSpace = 0;
if ( adjustForAlignment )
{
double labelWidthDiff = 0;
Expand All @@ -1368,8 +1420,14 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
labelWidthDiff = labelWidest - labelWidth;
break;

case AlignLeft:
case AlignJustify:
if ( !isFinalLine && labelWidest > labelWidth )
{
calculateExtraSpacingForLineJustification( labelWidest - labelWidth, block, extraWordSpace, extraLetterSpace );
}
break;

case AlignLeft:
break;
}

Expand Down Expand Up @@ -1399,7 +1457,6 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
}
break;
}
//QgsDebugMsgLevel( QStringLiteral( "xMultiLineOffset: %1" ).arg( xMultiLineOffset ), 4 );
}

double yMultiLineOffset = ascentOffset;
Expand Down Expand Up @@ -1434,6 +1491,8 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
subComponent.offset = QPointF( 0.0, -ascentOffset );
subComponent.rotation = -component.rotation * 180 / M_PI;
subComponent.rotationOffset = 0.0;
subComponent.extraWordSpacing = extraWordSpace * fontScale;
subComponent.extraLetterSpacing = extraLetterSpace * fontScale;

// draw the mask below the text (for preview)
if ( format.mask().enabled() )
Expand Down Expand Up @@ -1464,7 +1523,9 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con

QFont fragmentFont = font;
fragment.characterFormat().updateFontForFormat( fragmentFont, fontScale );
QFontMetricsF fragmentMetrics = QFontMetricsF( fragmentFont );

if ( extraWordSpace || extraLetterSpace )
applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );

path.addText( xOffset, 0, fragmentFont, fragment.text() );

Expand All @@ -1474,12 +1535,6 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
textp.drawPath( path );

xOffset += fragment.horizontalAdvance( fragmentFont, true );

// TODO: why are some font settings lost on drawPicture() when using drawText() inside QPicture?
// e.g. some capitalization options, but not others
//textp.setFont( tmpLyr.textFont );
//textp.setPen( tmpLyr.textColor );
//textp.drawText( 0, 0, component.text() );
}
textp.end();

Expand Down Expand Up @@ -1519,6 +1574,9 @@ void QgsTextRenderer::drawTextInternalHorizontal( QgsRenderContext &context, con
QFont fragmentFont = font;
fragment.characterFormat().updateFontForFormat( fragmentFont, fontScale );

if ( extraWordSpace || extraLetterSpace )
applyExtraSpacingForLineJustification( fragmentFont, extraWordSpace * fontScale, extraLetterSpace * fontScale );

QColor textColor = fragment.characterFormat().textColor().isValid() ? fragment.characterFormat().textColor() : format.color();
textColor.setAlphaF( format.opacity() );

Expand Down
8 changes: 8 additions & 0 deletions src/core/textrenderer/qgstextrenderer.h
Expand Up @@ -271,6 +271,11 @@ class CORE_EXPORT QgsTextRenderer
double dpiRatio = 1.0;
//! Horizontal alignment
HAlignment hAlign = AlignLeft;

//! Any additional word spacing to apply while rendering component
double extraWordSpacing = 0;
//! Any additional letter spacing to apply while rendering component
double extraLetterSpacing = 0;
};

static double textWidth( const QgsRenderContext &context, const QgsTextFormat &format, const QgsTextDocument &document );
Expand Down Expand Up @@ -347,6 +352,9 @@ class CORE_EXPORT QgsTextRenderer

static QgsTextFormat::TextOrientation calculateRotationAndOrientationForComponent( const QgsTextFormat &format, const Component &component, double &rotation );

static void calculateExtraSpacingForLineJustification( double spaceToDistribute, const QgsTextBlock &block, double &extraWordSpace, double &extraLetterSpace );
static void applyExtraSpacingForLineJustification( QFont &font, double extraWordSpace, double extraLetterSpace );

static void drawTextInternalHorizontal( QgsRenderContext &context,
const QgsTextFormat &format,
TextPart drawType,
Expand Down
21 changes: 20 additions & 1 deletion tests/src/python/test_qgstextrenderer.py
Expand Up @@ -2146,7 +2146,10 @@ def testDrawTextRectMultilineJustifyAlign(self):
format.setFont(getTestFont('bold'))
format.setSize(30)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
assert self.checkRender(format, 'text_rect_multiline_justify_aligned', text=['a test', 'of', 'justification', 'align'],
format.buffer().setEnabled(True)
format.buffer().setSize(4)
format.buffer().setSizeUnit(QgsUnitTypes.RenderMillimeters)
assert self.checkRender(format, 'text_rect_multiline_justify_aligned', text=['a t est', 'off', 'justification', 'align'],
alignment=QgsTextRenderer.AlignJustify, rect=QRectF(100, 100, 200, 100))

def testDrawTextRectJustifyAlign(self):
Expand Down Expand Up @@ -2234,6 +2237,22 @@ def testDrawTextPointRightAlign(self):
assert self.checkRenderPoint(format, 'text_point_right_aligned', text=['test'],
alignment=QgsTextRenderer.AlignRight, point=QPointF(300, 200))

def testDrawTextPointJustifyAlign(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(30)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
assert self.checkRenderPoint(format, 'text_point_justify_aligned', text=['test'],
alignment=QgsTextRenderer.AlignJustify, point=QPointF(100, 200))

def testDrawTextPointMultilineJustifyAlign(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
format.setSize(30)
format.setSizeUnit(QgsUnitTypes.RenderPoints)
assert self.checkRenderPoint(format, 'text_point_justify_multiline_aligned', text=['a t est', 'off', 'justification', 'align'],
alignment=QgsTextRenderer.AlignJustify, point=QPointF(100, 200))

def testDrawTextPointCenterAlign(self):
format = QgsTextFormat()
format.setFont(getTestFont('bold'))
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.
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 445beba

Please sign in to comment.