QGIS API Documentation  2.99.0-Master (6a61179)
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 
29 bool QgsFontUtils::fontMatchOnSystem( const QFont& f )
30 {
31  QFontInfo fi = QFontInfo( f );
32  return fi.exactMatch();
33 }
34 
35 bool QgsFontUtils::fontFamilyOnSystem( const QString& family )
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 QStringLiteral( "QGIS Vera Sans" );
219 }
220 
221 bool QgsFontUtils::loadStandardTestFonts( const QStringList& loadstyles )
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( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
229  fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
230  fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
231  fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
232 
233  QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
234  for ( ; f != fontStyles.constEnd(); ++f )
235  {
236  QString fontstyle( f.key() );
237  QString fontpath( f.value() );
238  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
239  {
240  continue;
241  }
242 
243  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
244  {
245  fontsLoaded = ( fontsLoaded || false );
246  QgsDebugMsg( QString( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ) );
247  }
248  else
249  {
250  bool loaded = false;
252  {
253  // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
254  // from qrc resources load but fail to work and default font is substituted [LS]:
255  // https://bugreports.qt-project.org/browse/QTBUG-30917
256  // https://bugreports.qt-project.org/browse/QTBUG-32789
257  QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
258  int fontID = QFontDatabase::addApplicationFont( fontPath );
259  loaded = ( fontID != -1 );
260  fontsLoaded = ( fontsLoaded || loaded );
261  QgsDebugMsg( QString( "Test font '%1 %2' %3 from filesystem [%4]" )
262  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ) );
263  QFontDatabase db;
264  QgsDebugMsg( QString( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ) );
265  }
266  else
267  {
268  QFile fontResource( ":/testdata/font/" + fontpath );
269  if ( fontResource.open( QIODevice::ReadOnly ) )
270  {
271  int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
272  loaded = ( fontID != -1 );
273  fontsLoaded = ( fontsLoaded || loaded );
274  }
275  QgsDebugMsg( QString( "Test font '%1' %3 from testdata.qrc" )
276  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ) );
277  }
278  }
279  }
280 
281  return fontsLoaded;
282 }
283 
284 QFont QgsFontUtils::getStandardTestFont( const QString& style, int pointsize )
285 {
286  if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
287  {
288  loadStandardTestFonts( QStringList() << style );
289  }
290 
291  QFontDatabase fontDB;
292  QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
293 #ifdef Q_OS_WIN
294  if ( !f.exactMatch() )
295  {
296  QString modified;
297  if ( style == "Roman" )
298  modified = "Normal";
299  else if ( style == "Oblique" )
300  modified = "Italic";
301  else if ( style == "Bold Oblique" )
302  modified = "Bold Italic";
303  if ( !modified.isEmpty() )
304  f = fontDB.font( standardTestFontFamily(), modified, pointsize );
305  }
306  if ( !f.exactMatch() )
307  {
308  QgsDebugMsg( QString( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
309  QgsDebugMsg( QString( "Requested: %1" ).arg( f.toString() ) );
310  QFontInfo fi( f );
311  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() ) );
312  }
313 #endif
314  // in case above statement fails to set style
315  f.setBold( style.contains( QLatin1String( "Bold" ) ) );
316  f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "Italic" ) ) );
317 
318  return f;
319 }
320 
321 QDomElement QgsFontUtils::toXmlElement( const QFont& font, QDomDocument& document, const QString& elementName )
322 {
323  QDomElement fontElem = document.createElement( elementName );
324  fontElem.setAttribute( QStringLiteral( "description" ), font.toString() );
325  fontElem.setAttribute( QStringLiteral( "style" ), untranslateNamedStyle( font.styleName() ) );
326  return fontElem;
327 }
328 
329 bool QgsFontUtils::setFromXmlElement( QFont& font, const QDomElement& element )
330 {
331  if ( element.isNull() )
332  {
333  return false;
334  }
335 
336  font.fromString( element.attribute( QStringLiteral( "description" ) ) );
337  if ( element.hasAttribute( QStringLiteral( "style" ) ) )
338  {
339  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "style" ) ) ) );
340  }
341 
342  return true;
343 }
344 
345 bool QgsFontUtils::setFromXmlChildNode( QFont& font, const QDomElement& element, const QString& childNode )
346 {
347  if ( element.isNull() )
348  {
349  return false;
350  }
351 
352  QDomNodeList nodeList = element.elementsByTagName( childNode );
353  if ( !nodeList.isEmpty() )
354  {
355  QDomElement fontElem = nodeList.at( 0 ).toElement();
356  return setFromXmlElement( font, fontElem );
357  }
358  else
359  {
360  return false;
361  }
362 }
363 
364 static QMap<QString, QString> createTranslatedStyleMap()
365 {
366  QMap<QString, QString> translatedStyleMap;
367  QStringList words = QStringList() << QStringLiteral( "Normal" ) << QStringLiteral( "Light" ) << QStringLiteral( "Bold" ) << QStringLiteral( "Black" ) << QStringLiteral( "Demi" ) << QStringLiteral( "Italic" ) << QStringLiteral( "Oblique" );
368  Q_FOREACH ( const QString& word, words )
369  {
370  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
371  }
372  return translatedStyleMap;
373 }
374 
375 QString QgsFontUtils::translateNamedStyle( const QString& namedStyle )
376 {
377  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
378  for ( int i = 0, n = words.length(); i < n; ++i )
379  {
380  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toUtf8(), nullptr, QCoreApplication::UnicodeUTF8 );
381  }
382  return words.join( QStringLiteral( " " ) );
383 }
384 
385 QString QgsFontUtils::untranslateNamedStyle( const QString& namedStyle )
386 {
387  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
388  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
389  for ( int i = 0, n = words.length(); i < n; ++i )
390  {
391  if ( translatedStyleMap.contains( words[i] ) )
392  {
393  words[i] = translatedStyleMap.value( words[i] );
394  }
395  else
396  {
397  QgsDebugMsg( QString( "Warning: style map does not contain %1" ).arg( words[i] ) );
398  }
399  }
400  return words.join( QStringLiteral( " " ) );
401 }
402 
403 QString QgsFontUtils::asCSS( const QFont& font, double pointToPixelScale )
404 {
405  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
406 
407  //style
408  css += QLatin1String( "font-style: " );
409  switch ( font.style() )
410  {
411  case QFont::StyleNormal:
412  css += QLatin1String( "normal" );
413  break;
414  case QFont::StyleItalic:
415  css += QLatin1String( "italic" );
416  break;
417  case QFont::StyleOblique:
418  css += QLatin1String( "oblique" );
419  break;
420  }
421  css += ';';
422 
423  //weight
424  int cssWeight = 400;
425  switch ( font.weight() )
426  {
427  case QFont::Light:
428  cssWeight = 300;
429  break;
430  case QFont::Normal:
431  cssWeight = 400;
432  break;
433  case QFont::DemiBold:
434  cssWeight = 600;
435  break;
436  case QFont::Bold:
437  cssWeight = 700;
438  break;
439  case QFont::Black:
440  cssWeight = 900;
441  break;
442 #if QT_VERSION >= 0x050500
443  case QFont::Thin:
444  cssWeight = 100;
445  break;
446  case QFont::ExtraLight:
447  cssWeight = 200;
448  break;
449  case QFont::Medium:
450  cssWeight = 500;
451  break;
452  case QFont::ExtraBold:
453  cssWeight = 800;
454  break;
455 #endif
456  }
457  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
458 
459  //size
460  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
461 
462  return css;
463 }
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
static bool setFromXmlElement(QFont &font, const QDomElement &element)
Sets the properties of a font to match the properties stored in an XML element.
static bool fontFamilyHasStyle(const QString &family, const QString &style)
Check whether font family on system has specific style.
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:196
static QString asCSS(const QFont &font, double pointToPixelMultiplier=1.0)
Returns a CSS string representing the specified font as closely as possible.
static bool fontFamilyOnSystem(const QString &family)
Check whether font family is on system in a quick manner, which does not compare [foundry].
static QFont getStandardTestFont(const QString &style="Roman", int pointsize=12)
Get standard test font with specific style.
static QMap< QString, QString > createTranslatedStyleMap()
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.
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. Valid only when running from build directory.
static bool fontFamilyMatchOnSystem(const QString &family, QString *chosen=nullptr, bool *match=nullptr)
Check whether font family is on system.
static bool updateFontViaStyle(QFont &f, const QString &fontstyle, bool fallback=false)
Updates font with named style and retain all font properties.
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.