Skip to content

Commit

Permalink
Fix Capitalize First Letter fails with curved labels (fix #14875)
Browse files Browse the repository at this point in the history
Instead of using QFont's inbuilt capitalization support, which
applies only on rendering and accordingly fails for curved
labels which are drawn one character at a time, we now manually
capitalize label text while registering features.

The capitalize first method from Qt was reimplemented in QgsStringUtils
(together with what I expect is better handling of unicode characters
over the Qt method).

This change also makes it possible to implement other capitalization
methods not directly supported by Qt

(cherry-picked from 15dd295)
  • Loading branch information
nyalldawson committed Aug 30, 2016
1 parent 577913e commit 54a50cc
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 34 deletions.
19 changes: 19 additions & 0 deletions python/core/qgsstringutils.sip
Expand Up @@ -10,6 +10,25 @@ class QgsStringUtils
#include <qgsstringutils.h>
%End
public:


//! Capitalization options
enum Capitalization
{
MixedCase, //!< Mixed case, ie no change
AllUppercase, //!< Convert all characters to uppercase
AllLowercase, //!< Convert all characters to lowercase
ForceFirstLetterToCapital, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
};

/** Converts a string by applying capitalization rules to the string.
* @param string input string
* @param capitalization capitalization type to apply
* @return capitalized string
* @note added in QGIS 3.0
*/
static QString capitalize( const QString& string, Capitalization capitalization );

/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
* number of character edits (insertions, deletions or substitutions) required to change
* one string to another.
Expand Down
71 changes: 37 additions & 34 deletions src/core/qgspallabeling.cpp
Expand Up @@ -18,6 +18,7 @@
#include "qgspallabeling.h"
#include "qgstextlabelfeature.h"
#include "qgsunittypes.h"
#include "qgsstringutils.h"

#include <list>

Expand Down Expand Up @@ -2283,6 +2284,7 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont

// calculate rest of font attributes and store any data defined values
// this is done here for later use in making label backgrounds part of collision management (when implemented)
labelFont.setCapitalization( QFont::MixedCase ); // reset this - we don't use QFont's handling as it breaks with curved labels
parseTextStyle( labelFont, fontunits, context );
parseTextFormatting( context );
parseTextBuffer( context );
Expand Down Expand Up @@ -2316,6 +2318,41 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
labelText = v.isNull() ? "" : v.toString();
}

// apply capitalization
QgsStringUtils::Capitalization capitalization = QgsStringUtils::MixedCase;
// maintain API - capitalization may have been set in textFont
if ( textFont.capitalization() != QFont::MixedCase )
{
capitalization = static_cast< QgsStringUtils::Capitalization >( textFont.capitalization() );
}
// data defined font capitalization?
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
{
QString fcase = exprVal.toString().trimmed();
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );

if ( !fcase.isEmpty() )
{
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::MixedCase;
}
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::AllUppercase;
}
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::AllLowercase;
}
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
{
capitalization = QgsStringUtils::ForceFirstLetterToCapital;
}
}
}
labelText = QgsStringUtils::capitalize( labelText, capitalization );

// data defined format numbers?
bool formatnum = formatNumbers;
if ( dataDefinedEvaluate( QgsPalLayerSettings::NumFormat, exprVal, &context.expressionContext(), formatNumbers ) )
Expand Down Expand Up @@ -3356,7 +3393,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
// copy over existing font settings
//newFont = newFont.resolve( labelFont ); // should work, but let's be sure what's being copied
newFont.setPixelSize( labelFont.pixelSize() );
newFont.setCapitalization( labelFont.capitalization() );
newFont.setUnderline( labelFont.underline() );
newFont.setStrikeOut( labelFont.strikeOut() );
newFont.setWordSpacing( labelFont.wordSpacing() );
Expand Down Expand Up @@ -3393,39 +3429,6 @@ void QgsPalLayerSettings::parseTextStyle( QFont& labelFont,
}
labelFont.setLetterSpacing( QFont::AbsoluteSpacing, scaleToPixelContext( letterspace, context, fontunits, false, fontSizeMapUnitScale ) );

// data defined font capitalization?
QFont::Capitalization fontcaps = labelFont.capitalization();
if ( dataDefinedEvaluate( QgsPalLayerSettings::FontCase, exprVal, &context.expressionContext() ) )
{
QString fcase = exprVal.toString().trimmed();
QgsDebugMsgLevel( QString( "exprVal FontCase:%1" ).arg( fcase ), 4 );

if ( !fcase.isEmpty() )
{
if ( fcase.compare( "NoChange", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::MixedCase;
}
else if ( fcase.compare( "Upper", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::AllUppercase;
}
else if ( fcase.compare( "Lower", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::AllLowercase;
}
else if ( fcase.compare( "Capitalize", Qt::CaseInsensitive ) == 0 )
{
fontcaps = QFont::Capitalize;
}

if ( fontcaps != labelFont.capitalization() )
{
labelFont.setCapitalization( fontcaps );
}
}
}

// data defined strikeout font style?
if ( dataDefinedEvaluate( QgsPalLayerSettings::Strikeout, exprVal, &context.expressionContext(), labelFont.strikeOut() ) )
{
Expand Down
50 changes: 50 additions & 0 deletions src/core/qgsstringutils.cpp
Expand Up @@ -15,6 +15,56 @@

#include "qgsstringutils.h"
#include <QVector>
#include <QRegExp>
#include <QStringList>
#include <QTextBoundaryFinder>

QString QgsStringUtils::capitalize( const QString& string, QgsStringUtils::Capitalization capitalization )
{
if ( string.isEmpty() )
return QString();

switch ( capitalization )
{
case MixedCase:
return string;

case AllUppercase:
return string.toUpper();

case AllLowercase:
return string.toLower();

case ForceFirstLetterToCapital:
{
QString temp = string;

QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word, string.constData(), string.length(), 0, 0 );
QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme, string.constData(), string.length(), 0, 0 );

wordSplitter.setPosition( 0 );
bool first = true;
#if QT_VERSION >= 0x050000
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
|| wordSplitter.toNextBoundary() >= 0 )
#else
while (( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartWord )
|| wordSplitter.toNextBoundary() >= 0 )
#endif
{
first = false;
letterSplitter.setPosition( wordSplitter.position() );
letterSplitter.toNextBoundary();
QString substr = string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
}
return temp;
}

}
// no warnings
return string;
}

int QgsStringUtils::levenshteinDistance( const QString& string1, const QString& string2, bool caseSensitive )
{
Expand Down
18 changes: 18 additions & 0 deletions src/core/qgsstringutils.h
Expand Up @@ -14,6 +14,7 @@
***************************************************************************/

#include <QString>
#include <QFont> // for enum values

#ifndef QGSSTRINGUTILS_H
#define QGSSTRINGUTILS_H
Expand All @@ -28,6 +29,23 @@ class CORE_EXPORT QgsStringUtils
{
public:

//! Capitalization options
enum Capitalization
{
MixedCase = QFont::MixedCase, //!< Mixed case, ie no change
AllUppercase = QFont::AllUppercase, //!< Convert all characters to uppercase
AllLowercase = QFont::AllLowercase, //!< Convert all characters to lowercase
ForceFirstLetterToCapital = QFont::Capitalize, //!< Convert just the first letter of each word to uppercase, leave the rest untouched
};

/** Converts a string by applying capitalization rules to the string.
* @param string input string
* @param capitalization capitalization type to apply
* @return capitalized string
* @note added in QGIS 3.0
*/
static QString capitalize( const QString& string, Capitalization capitalization );

/** Returns the Levenshtein edit distance between two strings. This equates to the minimum
* number of character edits (insertions, deletions or substitutions) required to change
* one string to another.
Expand Down
2 changes: 2 additions & 0 deletions src/core/qgsvectorlayerlabelprovider.h
Expand Up @@ -118,6 +118,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider

//! List of generated
QList<QgsLabelFeature*> mLabels;

friend class TestQgsLabelingEngineV2;
};

#endif // QGSVECTORLAYERLABELPROVIDER_H
60 changes: 60 additions & 0 deletions tests/src/core/testqgslabelingenginev2.cpp
Expand Up @@ -43,6 +43,7 @@ class TestQgsLabelingEngineV2 : public QObject
void testRuleBased();
void zOrder(); //test that labels are stacked correctly
void testEncodeDecodePositionOrder();
void testCapitalization();

private:
QgsVectorLayer* vl;
Expand Down Expand Up @@ -413,6 +414,65 @@ void TestQgsLabelingEngineV2::testEncodeDecodePositionOrder()
QCOMPARE( decoded, expected );
}

void TestQgsLabelingEngineV2::testCapitalization()
{
QgsFeature f( vl->fields(), 1 );
f.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 2 ) ) );

// make a fake render context
QSize size( 640, 480 );
QgsMapSettings mapSettings;
mapSettings.setOutputSize( size );
mapSettings.setExtent( vl->extent() );
mapSettings.setLayers( QStringList() << vl->id() );
mapSettings.setOutputDpi( 96 );
QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings );
QStringList attributes;
QgsLabelingEngineV2 engine;
engine.setMapSettings( mapSettings );

// no change
QgsPalLayerSettings settings;
QFont font = settings.textFont;
font.setCapitalization( QFont::MixedCase );
settings.textFont = font;
settings.fieldName = QString( "'a teSt LABEL'" );
settings.isExpression = true;

QgsVectorLayerLabelProvider* provider = new QgsVectorLayerLabelProvider( vl, "test", true, &settings );
engine.addProvider( provider );
provider->prepare( context, attributes );
provider->registerFeature( f, context );
QCOMPARE( provider->mLabels.at( 0 )->labelText(), QString( "a teSt LABEL" ) );

//uppercase
font.setCapitalization( QFont::AllUppercase );
settings.textFont = font;
QgsVectorLayerLabelProvider* provider2 = new QgsVectorLayerLabelProvider( vl, "test2", true, &settings );
engine.addProvider( provider2 );
provider2->prepare( context, attributes );
provider2->registerFeature( f, context );
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "A TEST LABEL" ) );

//lowercase
font.setCapitalization( QFont::AllLowercase );
settings.textFont = font;
QgsVectorLayerLabelProvider* provider3 = new QgsVectorLayerLabelProvider( vl, "test3", true, &settings );
engine.addProvider( provider3 );
provider3->prepare( context, attributes );
provider3->registerFeature( f, context );
QCOMPARE( provider3->mLabels.at( 0 )->labelText(), QString( "a test label" ) );

//first letter uppercase
font.setCapitalization( QFont::Capitalize );
settings.textFont = font;
QgsVectorLayerLabelProvider* provider4 = new QgsVectorLayerLabelProvider( vl, "test4", true, &settings );
engine.addProvider( provider4 );
provider4->prepare( context, attributes );
provider4->registerFeature( f, context );
QCOMPARE( provider4->mLabels.at( 0 )->labelText(), QString( "A TeSt LABEL" ) );
}

bool TestQgsLabelingEngineV2::imageCheck( const QString& testName, QImage &image, int mismatchCount )
{
//draw background
Expand Down

0 comments on commit 54a50cc

Please sign in to comment.