QGIS API Documentation
qgsfontutils.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsfontutils.h
3  ---------------------
4  begin : June 5, 2013
5  copyright : (C) 2013 by Larry Shaffer
6  email : larrys at dakotacarto dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgsfontutils.h"
17 
18 #include "qgsapplication.h"
19 #include "qgslogger.h"
20 
21 #include <QApplication>
22 #include <QFile>
23 #include <QFont>
24 #include <QFontDatabase>
25 #include <QFontInfo>
26 #include <QStringList>
27 
28 
30 {
31  QFontInfo fi = QFontInfo( f );
32  return fi.exactMatch();
33 }
34 
36 {
37  QFont tmpFont = QFont( family );
38  // compare just beginning of family string in case 'family [foundry]' differs
39  return tmpFont.family().startsWith( family, Qt::CaseInsensitive );
40 }
41 
42 bool QgsFontUtils::fontFamilyHasStyle( const QString& family, const QString& style )
43 {
44  QFontDatabase fontDB;
45  if ( !fontFamilyOnSystem( family ) )
46  return false;
47 
48  if ( fontDB.styles( family ).contains( style ) )
49  return true;
50 
51 #ifdef Q_OS_WIN
52  QString modified( style );
53  if ( style == "Roman" )
54  modified = "Normal";
55  if ( style == "Oblique" )
56  modified = "Italic";
57  if ( style == "Bold Oblique" )
58  modified = "Bold Italic";
59  if ( fontDB.styles( family ).contains( modified ) )
60  return true;
61 #endif
62 
63  return false;
64 }
65 
66 bool QgsFontUtils::fontFamilyMatchOnSystem( const QString& family, QString* chosen, bool* match )
67 {
68  QFontDatabase fontDB;
69  QStringList fontFamilies = fontDB.families();
70  bool found = false;
71 
72  QList<QString>::const_iterator it = fontFamilies.constBegin();
73  for ( ; it != fontFamilies.constEnd(); ++it )
74  {
75  // first compare just beginning of 'family [foundry]' string
76  if ( it->startsWith( family, Qt::CaseInsensitive ) )
77  {
78  found = true;
79  // keep looking if match info is requested
80  if ( match )
81  {
82  // full 'family [foundry]' strings have to match
83  *match = ( *it == family );
84  if ( *match )
85  break;
86  }
87  else
88  {
89  break;
90  }
91  }
92  }
93 
94  if ( found )
95  {
96  if ( chosen )
97  {
98  // retrieve the family actually assigned by matching algorithm
99  QFont f = QFont( family );
100  *chosen = f.family();
101  }
102  }
103  else
104  {
105  if ( chosen )
106  {
107  *chosen = QString();
108  }
109 
110  if ( match )
111  {
112  *match = false;
113  }
114  }
115 
116  return found;
117 }
118 
119 bool QgsFontUtils::updateFontViaStyle( QFont& f, const QString& fontstyle, bool fallback )
120 {
121  if ( fontstyle.isEmpty() )
122  {
123  return false;
124  }
125 
126  QFontDatabase fontDB;
127 
128  if ( !fallback )
129  {
130  // does the font even have the requested style?
131  bool hasstyle = fontFamilyHasStyle( f.family(), fontstyle );
132  if ( !hasstyle )
133  {
134  return false;
135  }
136  }
137 
138  // is the font's style already the same as requested?
139  if ( fontstyle == fontDB.styleString( f ) )
140  {
141  return false;
142  }
143 
144  QFont appfont = QApplication::font();
145  int defaultSize = appfont.pointSize(); // QFontDatabase::font() needs an integer for size
146 
147  QFont styledfont;
148  bool foundmatch = false;
149 
150  // if fontDB.font() fails, it returns the default app font; but, that may be the target style
151  styledfont = fontDB.font( f.family(), fontstyle, defaultSize );
152  if ( appfont != styledfont || fontstyle != fontDB.styleString( f ) )
153  {
154  foundmatch = true;
155  }
156 
157  // default to first found style if requested style is unavailable
158  // this helps in the situations where the passed-in font has to have a named style applied
159  if ( fallback && !foundmatch )
160  {
161  QFont testFont = QFont( f );
162  testFont.setPointSize( defaultSize );
163 
164  // prefer a style that mostly matches the passed-in font
165  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
166  {
167  styledfont = fontDB.font( f.family(), style, defaultSize );
168  styledfont = styledfont.resolve( f );
169  if ( testFont.toString() == styledfont.toString() )
170  {
171  foundmatch = true;
172  break;
173  }
174  }
175 
176  // fallback to first style found that works
177  if ( !foundmatch )
178  {
179  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
180  {
181  styledfont = fontDB.font( f.family(), style, defaultSize );
182  if ( QApplication::font() != styledfont )
183  {
184  foundmatch = true;
185  break;
186  }
187  }
188  }
189  }
190 
191  // similar to QFont::resolve, but font may already have pixel size set
192  // and we want to make sure that's preserved
193  if ( foundmatch )
194  {
195  if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
196  {
197  styledfont.setPointSizeF( f.pointSizeF() );
198  }
199  else if ( f.pixelSize() != -1 )
200  {
201  styledfont.setPixelSize( f.pixelSize() );
202  }
203  styledfont.setCapitalization( f.capitalization() );
204  styledfont.setUnderline( f.underline() );
205  styledfont.setStrikeOut( f.strikeOut() );
206  styledfont.setWordSpacing( f.wordSpacing() );
207  styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
208  f = styledfont;
209 
210  return true;
211  }
212 
213  return false;
214 }
215 
217 {
218  return "QGIS Vera Sans";
219 }
220 
222 {
223  // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
224  bool fontsLoaded = false;
225 
226  QString fontFamily = standardTestFontFamily();
227  QMap<QString, QString> fontStyles;
228  fontStyles.insert( "Roman", "QGIS-Vera/QGIS-Vera.ttf" );
229  fontStyles.insert( "Oblique", "QGIS-Vera/QGIS-VeraIt.ttf" );
230  fontStyles.insert( "Bold", "QGIS-Vera/QGIS-VeraBd.ttf" );
231  fontStyles.insert( "Bold Oblique", "QGIS-Vera/QGIS-VeraBI.ttf" );
232 
234  for ( ; f != fontStyles.constEnd(); ++f )
235  {
236  QString fontstyle( f.key() );
237  QString fontpath( f.value() );
238  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( "All" ) ) )
239  {
240  continue;
241  }
242  QString familyStyle = QString( "%1 %2" ).arg( fontFamily, fontstyle );
243 
244  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
245  {
246  fontsLoaded = ( fontsLoaded || false );
247  QgsDebugMsg( QString( "Test font '%1' already available" ).arg( familyStyle ) );
248  }
249  else
250  {
251  bool loaded = false;
253  {
254  // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
255  // from qrc resources load but fail to work and default font is substituted [LS]:
256  // https://bugreports.qt-project.org/browse/QTBUG-30917
257  // https://bugreports.qt-project.org/browse/QTBUG-32789
258  QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
259  int fontID = QFontDatabase::addApplicationFont( fontPath );
260  loaded = ( fontID != -1 );
261  fontsLoaded = ( fontsLoaded || loaded );
262  QgsDebugMsg( QString( "Test font '%1' %2 from filesystem [%3]" )
263  .arg( familyStyle, loaded ? "loaded" : "FAILED to load", fontPath ) );
264  QFontDatabase db;
265  QgsDebugMsg( QString( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ) );
266  }
267  else
268  {
269  QFile fontResource( ":/testdata/font/" + fontpath );
270  if ( fontResource.open( QIODevice::ReadOnly ) )
271  {
272  int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
273  loaded = ( fontID != -1 );
274  fontsLoaded = ( fontsLoaded || loaded );
275  }
276  QgsDebugMsg( QString( "Test font '%1' %2 from testdata.qrc" )
277  .arg( familyStyle, loaded ? "loaded" : "FAILED to load" ) );
278  }
279  }
280  }
281 
282  return fontsLoaded;
283 }
284 
285 QFont QgsFontUtils::getStandardTestFont( const QString& style, int pointsize )
286 {
287  if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
288  {
289  loadStandardTestFonts( QStringList() << style );
290  }
291 
292  QFontDatabase fontDB;
293  QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
294 #ifdef Q_OS_WIN
295  if ( !f.exactMatch() )
296  {
297  QString modified;
298  if ( style == "Roman" )
299  modified = "Normal";
300  else if ( style == "Oblique" )
301  modified = "Italic";
302  else if ( style == "Bold Oblique" )
303  modified = "Bold Italic";
304  if ( !modified.isEmpty() )
305  f = fontDB.font( standardTestFontFamily(), modified, pointsize );
306  }
307  if ( !f.exactMatch() )
308  {
309  QgsDebugMsg( QString( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
310  QgsDebugMsg( QString( "Requested: %1" ).arg( f.toString() ) );
311  QFontInfo fi( f );
312  QgsDebugMsg( QString( "Replaced: %1,%2,%3,%4,%5,%6,%7,%8,%9,%10" ).arg( fi.family() ).arg( fi.pointSizeF() ).arg( fi.pixelSize() ).arg( fi.styleHint() ).arg( fi.weight() ).arg( fi.style() ).arg( fi.underline() ).arg( fi.strikeOut() ).arg( fi.fixedPitch() ).arg( fi.rawMode() ) );
313  }
314 #endif
315  // in case above statement fails to set style
316  f.setBold( style.contains( "Bold" ) );
317  f.setItalic( style.contains( "Oblique" ) || style.contains( "Italic" ) );
318 
319  return f;
320 }
321 
322 QDomElement QgsFontUtils::toXmlElement( const QFont& font, QDomDocument& document, const QString& elementName )
323 {
324  QDomElement fontElem = document.createElement( elementName );
325  fontElem.setAttribute( "description", font.toString() );
326  fontElem.setAttribute( "style", untranslateNamedStyle( font.styleName() ) );
327  return fontElem;
328 }
329 
330 bool QgsFontUtils::setFromXmlElement( QFont& font, const QDomElement& element )
331 {
332  if ( element.isNull() )
333  {
334  return false;
335  }
336 
337  font.fromString( element.attribute( "description" ) );
338  if ( element.hasAttribute( "style" ) )
339  {
340  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( "style" ) ) );
341  }
342 
343  return true;
344 }
345 
346 bool QgsFontUtils::setFromXmlChildNode( QFont& font, const QDomElement& element, const QString& childNode )
347 {
348  if ( element.isNull() )
349  {
350  return false;
351  }
352 
353  QDomNodeList nodeList = element.elementsByTagName( childNode );
354  if ( !nodeList.isEmpty() )
355  {
356  QDomElement fontElem = nodeList.at( 0 ).toElement();
357  return setFromXmlElement( font, fontElem );
358  }
359  else
360  {
361  return false;
362  }
363 }
364 
366 {
367  QMap<QString, QString> translatedStyleMap;
368  QStringList words = QStringList() << "Normal" << "Light" << "Bold" << "Black" << "Demi" << "Italic" << "Oblique";
369  Q_FOREACH ( const QString& word, words )
370  {
371  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
372  }
373  return translatedStyleMap;
374 }
375 
377 {
378  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
379  for ( int i = 0, n = words.length(); i < n; ++i )
380  {
381  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toUtf8(), nullptr, QCoreApplication::UnicodeUTF8 );
382  }
383  return words.join( " " );
384 }
385 
387 {
388  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
389  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
390  for ( int i = 0, n = words.length(); i < n; ++i )
391  {
392  if ( translatedStyleMap.contains( words[i] ) )
393  {
394  words[i] = translatedStyleMap.value( words[i] );
395  }
396  else
397  {
398  QgsDebugMsg( QString( "Warning: style map does not contain %1" ).arg( words[i] ) );
399  }
400  }
401  return words.join( " " );
402 }
403 
404 QString QgsFontUtils::asCSS( const QFont& font, double pointToPixelScale )
405 {
406  QString css = QString( "font-family: " ) + font.family() + ';';
407 
408  //style
409  css += "font-style: ";
410  switch ( font.style() )
411  {
412  case QFont::StyleNormal:
413  css += "normal";
414  break;
415  case QFont::StyleItalic:
416  css += "italic";
417  break;
418  case QFont::StyleOblique:
419  css += "oblique";
420  break;
421  }
422  css += ';';
423 
424  //weight
425  int cssWeight = 400;
426  switch ( font.weight() )
427  {
428  case QFont::Light:
429  cssWeight = 300;
430  break;
431  case QFont::Normal:
432  cssWeight = 400;
433  break;
434  case QFont::DemiBold:
435  cssWeight = 600;
436  break;
437  case QFont::Bold:
438  cssWeight = 700;
439  break;
440  case QFont::Black:
441  cssWeight = 900;
442  break;
443 #if QT_VERSION >= 0x050500
444  case QFont::Thin:
445  cssWeight = 100;
446  break;
447  case QFont::ExtraLight:
448  cssWeight = 200;
449  break;
450  case QFont::Medium:
451  cssWeight = 500;
452  break;
453  case QFont::ExtraBold:
454  cssWeight = 800;
455  break;
456 #endif
457  }
458  css += QString( "font-weight: %1;" ).arg( cssWeight );
459 
460  //size
461  css += QString( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
462 
463  return css;
464 }
QDomNodeList elementsByTagName(const QString &tagname) const
void setPointSize(int pointSize)
int addApplicationFontFromData(const QByteArray &fontData)
bool contains(const Key &key) const
int pixelSize() const
QString attribute(const QString &name, const QString &defValue) const
int length() const
qreal pointSizeF() const
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
int weight() const
QStringList split(const QString &sep, SplitBehavior behavior, Qt::CaseSensitivity cs) const
const_iterator constBegin() const
static bool setFromXmlElement(QFont &font, const QDomElement &element)
Sets the properties of a font to match the properties stored in an XML element.
bool contains(const QString &str, Qt::CaseSensitivity cs) const
void setUnderline(bool enable)
static bool fontFamilyHasStyle(const QString &family, const QString &style)
Check whether font family on system has specific style.
Capitalization capitalization() const
QString join(const QString &separator) const
static bool isRunningFromBuildDir()
Indicates whether running from build directory (not installed)
static QString translateNamedStyle(const QString &namedStyle)
Returns the localized named style of a font, if such a translation is available.
bool qgsDoubleNear(double a, double b, double epsilon=4 *DBL_EPSILON)
Compare two doubles (but allow some difference)
Definition: qgis.h:348
QStringList styles(const QString &family) const
static QString asCSS(const QFont &font, double pointToPixelMultiplier=1.0)
Returns a CSS string representing the specified font as closely as possible.
QString styleName() const
QDomElement toElement() const
void setBold(bool enable)
bool isEmpty() const
void setPixelSize(int pixelSize)
int pixelSize() const
QFont font()
bool fromString(const QString &descrip)
bool hasAttribute(const QString &name) const
static bool fontFamilyOnSystem(const QString &family)
Check whether font family is on system in a quick manner, which does not compare [foundry].
int weight() const
qreal letterSpacing() const
void setAttribute(const QString &name, const QString &value)
QStringList applicationFontFamilies(int id)
int addApplicationFont(const QString &fileName)
static QFont getStandardTestFont(const QString &style="Roman", int pointsize=12)
Get standard test font with specific style.
bool isEmpty() const
const_iterator constEnd() const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const
QByteArray readAll()
QFont resolve(const QFont &other) const
static QMap< QString, QString > createTranslatedStyleMap()
bool underline() const
static bool setFromXmlChildNode(QFont &font, const QDomElement &element, const QString &childNode)
Sets the properties of a font to match the properties stored in an XML child node.
static bool loadStandardTestFonts(const QStringList &loadstyles)
Loads standard test fonts from filesystem or qrc resource.
static QString standardTestFontFamily()
Get standard test font family.
virtual bool open(QFlags< QIODevice::OpenModeFlag > mode)
QString styleString(const QFont &font)
QString family() const
void setWordSpacing(qreal spacing)
bool startsWith(const T &value) const
bool contains(QChar ch, Qt::CaseSensitivity cs) const
bool rawMode() const
QFont::Style style() const
bool exactMatch() const
void setItalic(bool enable)
void setPointSizeF(qreal pointSize)
bool isNull() const
const Key key(const T &value) const
static QString untranslateNamedStyle(const QString &namedStyle)
Returns the english named style of a font, if possible.
static QString buildSourcePath()
Returns path to the source directory.
static bool fontFamilyMatchOnSystem(const QString &family, QString *chosen=nullptr, bool *match=nullptr)
Check whether font family is on system.
QString toString() const
QFont::StyleHint styleHint() const
QStringList families(WritingSystem writingSystem) const
QString family() const
static bool updateFontViaStyle(QFont &f, const QString &fontstyle, bool fallback=false)
Updates font with named style and retain all font properties.
QString translate(const char *context, const char *sourceText, const char *disambiguation, Encoding encoding)
void setStrikeOut(bool enable)
void setCapitalization(Capitalization caps)
qreal pointSizeF() const
iterator insert(const Key &key, const T &value)
static bool fontMatchOnSystem(const QFont &f)
Check whether exact font is on system.
static QDomElement toXmlElement(const QFont &font, QDomDocument &document, const QString &elementName)
Returns a DOM element containing the properties of the font.
const_iterator constEnd() const
QDomElement createElement(const QString &tagName)
bool strikeOut() const
const_iterator constBegin() const
QFont font(const QString &family, const QString &style, int pointSize) const
bool exactMatch() const
void setLetterSpacing(SpacingType type, qreal spacing)
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
bool fixedPitch() const
int pointSize() const
Style style() const
QDomNode at(int index) const
qreal wordSpacing() const
const T value(const Key &key) const