Skip to content
Permalink
Browse files
Fix Capitalize First Letter fails with curved labels (fix #14875)
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 b0c67cd commit 5cc97ab
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 34 deletions.
@@ -122,6 +122,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.
@@ -2306,6 +2306,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 );
@@ -2345,6 +2346,41 @@ void QgsPalLayerSettings::registerFeature( QgsFeature& f, QgsRenderContext &cont
labelText = substitutions.process( labelText );
}

// 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 ) )
@@ -3396,7 +3432,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() );
@@ -3433,39 +3468,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() ) )
{
@@ -17,6 +17,55 @@
#include <QVector>
#include <QRegExp>
#include <QTextDocument> // for Qt::escape
#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 )
{
@@ -17,6 +17,7 @@
#include <QRegExp>
#include <QList>
#include <QDomDocument>
#include <QFont> // for enum values
#include "qgis.h"

#ifndef QGSSTRINGUTILS_H
@@ -166,6 +167,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.
@@ -44,6 +44,7 @@ class TestQgsLabelingEngineV2 : public QObject
void zOrder(); //test that labels are stacked correctly
void testEncodeDecodePositionOrder();
void testSubstitutions();
void testCapitalization();

private:
QgsVectorLayer* vl;
@@ -454,6 +455,66 @@ void TestQgsLabelingEngineV2::testSubstitutions()
QCOMPARE( provider2->mLabels.at( 0 )->labelText(), QString( "bb label" ) );
}

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
@@ -133,5 +133,49 @@ def testSaveRestore(self):
self.assertEqual(c2.replacements(), c.replacements())


class PyQgsStringUtils(unittest.TestCase):

def testMixed(self):
""" test mixed capitalization - ie, no change! """
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.MixedCase))
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.MixedCase), '')
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.MixedCase), 'testing 123')
self.assertEqual(QgsStringUtils.capitalize(' tESTinG 123 ', QgsStringUtils.MixedCase), ' tESTinG 123 ')
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.MixedCase), ' TESTING ABC')

def testUpperCase(self):
""" test uppercase """
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.AllUppercase))
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.AllUppercase), '')
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.AllUppercase), 'TESTING 123')
self.assertEqual(QgsStringUtils.capitalize(' tESTinG abc ', QgsStringUtils.AllUppercase), ' TESTING ABC ')
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.AllUppercase), ' TESTING ABC')

def testLowerCase(self):
""" test lowercase """
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.AllLowercase))
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.AllLowercase), '')
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.AllLowercase), 'testing 123')
self.assertEqual(QgsStringUtils.capitalize(' tESTinG abc ', QgsStringUtils.AllLowercase),
' testing abc ')
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.AllLowercase), ' testing abc')

def testCapitalizeFirst(self):
""" test capitalize first """
self.assertFalse(QgsStringUtils.capitalize(None, QgsStringUtils.ForceFirstLetterToCapital))
self.assertEqual(QgsStringUtils.capitalize('', QgsStringUtils.ForceFirstLetterToCapital), '')
self.assertEqual(QgsStringUtils.capitalize('testing 123', QgsStringUtils.ForceFirstLetterToCapital), 'Testing 123')
self.assertEqual(QgsStringUtils.capitalize('testing', QgsStringUtils.ForceFirstLetterToCapital),
'Testing')
self.assertEqual(QgsStringUtils.capitalize('Testing', QgsStringUtils.ForceFirstLetterToCapital),
'Testing')
self.assertEqual(QgsStringUtils.capitalize('TESTING', QgsStringUtils.ForceFirstLetterToCapital),
'TESTING')
self.assertEqual(QgsStringUtils.capitalize(' tESTinG abc ', QgsStringUtils.ForceFirstLetterToCapital),
' TESTinG Abc ')
self.assertEqual(QgsStringUtils.capitalize(' TESTING ABC', QgsStringUtils.ForceFirstLetterToCapital), ' TESTING ABC')
self.assertEqual(QgsStringUtils.capitalize(' testing abc', QgsStringUtils.ForceFirstLetterToCapital),
' Testing Abc')

if __name__ == '__main__':
unittest.main()

0 comments on commit 5cc97ab

Please sign in to comment.