QGIS API Documentation  3.2.0-Bonn (bc43194)
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 #include "qgssettings.h"
21 
22 #include <QApplication>
23 #include <QFile>
24 #include <QFont>
25 #include <QFontDatabase>
26 #include <QFontInfo>
27 #include <QStringList>
28 #include <QMimeData>
29 #include <memory>
30 
31 bool QgsFontUtils::fontMatchOnSystem( const QFont &f )
32 {
33  QFontInfo fi = QFontInfo( f );
34  return fi.exactMatch();
35 }
36 
37 bool QgsFontUtils::fontFamilyOnSystem( const QString &family )
38 {
39  QFont tmpFont = QFont( family );
40  // compare just beginning of family string in case 'family [foundry]' differs
41  return tmpFont.family().startsWith( family, Qt::CaseInsensitive );
42 }
43 
44 bool QgsFontUtils::fontFamilyHasStyle( const QString &family, const QString &style )
45 {
46  QFontDatabase fontDB;
47  if ( !fontFamilyOnSystem( family ) )
48  return false;
49 
50  if ( fontDB.styles( family ).contains( style ) )
51  return true;
52 
53 #ifdef Q_OS_WIN
54  QString modified( style );
55  if ( style == "Roman" )
56  modified = "Normal";
57  if ( style == "Oblique" )
58  modified = "Italic";
59  if ( style == "Bold Oblique" )
60  modified = "Bold Italic";
61  if ( fontDB.styles( family ).contains( modified ) )
62  return true;
63 #endif
64 
65  return false;
66 }
67 
68 bool QgsFontUtils::fontFamilyMatchOnSystem( const QString &family, QString *chosen, bool *match )
69 {
70  QFontDatabase fontDB;
71  QStringList fontFamilies = fontDB.families();
72  bool found = false;
73 
74  QList<QString>::const_iterator it = fontFamilies.constBegin();
75  for ( ; it != fontFamilies.constEnd(); ++it )
76  {
77  // first compare just beginning of 'family [foundry]' string
78  if ( it->startsWith( family, Qt::CaseInsensitive ) )
79  {
80  found = true;
81  // keep looking if match info is requested
82  if ( match )
83  {
84  // full 'family [foundry]' strings have to match
85  *match = ( *it == family );
86  if ( *match )
87  break;
88  }
89  else
90  {
91  break;
92  }
93  }
94  }
95 
96  if ( found )
97  {
98  if ( chosen )
99  {
100  // retrieve the family actually assigned by matching algorithm
101  QFont f = QFont( family );
102  *chosen = f.family();
103  }
104  }
105  else
106  {
107  if ( chosen )
108  {
109  *chosen = QString();
110  }
111 
112  if ( match )
113  {
114  *match = false;
115  }
116  }
117 
118  return found;
119 }
120 
121 bool QgsFontUtils::updateFontViaStyle( QFont &f, const QString &fontstyle, bool fallback )
122 {
123  if ( fontstyle.isEmpty() )
124  {
125  return false;
126  }
127 
128  QFontDatabase fontDB;
129 
130  if ( !fallback )
131  {
132  // does the font even have the requested style?
133  bool hasstyle = fontFamilyHasStyle( f.family(), fontstyle );
134  if ( !hasstyle )
135  {
136  return false;
137  }
138  }
139 
140  // is the font's style already the same as requested?
141  if ( fontstyle == fontDB.styleString( f ) )
142  {
143  return false;
144  }
145 
146  QFont appfont = QApplication::font();
147  int defaultSize = appfont.pointSize(); // QFontDatabase::font() needs an integer for size
148 
149  QFont styledfont;
150  bool foundmatch = false;
151 
152  // if fontDB.font() fails, it returns the default app font; but, that may be the target style
153  styledfont = fontDB.font( f.family(), fontstyle, defaultSize );
154  if ( appfont != styledfont || fontstyle != fontDB.styleString( f ) )
155  {
156  foundmatch = true;
157  }
158 
159  // default to first found style if requested style is unavailable
160  // this helps in the situations where the passed-in font has to have a named style applied
161  if ( fallback && !foundmatch )
162  {
163  QFont testFont = QFont( f );
164  testFont.setPointSize( defaultSize );
165 
166  // prefer a style that mostly matches the passed-in font
167  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
168  {
169  styledfont = fontDB.font( f.family(), style, defaultSize );
170  styledfont = styledfont.resolve( f );
171  if ( testFont.toString() == styledfont.toString() )
172  {
173  foundmatch = true;
174  break;
175  }
176  }
177 
178  // fallback to first style found that works
179  if ( !foundmatch )
180  {
181  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
182  {
183  styledfont = fontDB.font( f.family(), style, defaultSize );
184  if ( QApplication::font() != styledfont )
185  {
186  foundmatch = true;
187  break;
188  }
189  }
190  }
191  }
192 
193  // similar to QFont::resolve, but font may already have pixel size set
194  // and we want to make sure that's preserved
195  if ( foundmatch )
196  {
197  if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
198  {
199  styledfont.setPointSizeF( f.pointSizeF() );
200  }
201  else if ( f.pixelSize() != -1 )
202  {
203  styledfont.setPixelSize( f.pixelSize() );
204  }
205  styledfont.setCapitalization( f.capitalization() );
206  styledfont.setUnderline( f.underline() );
207  styledfont.setStrikeOut( f.strikeOut() );
208  styledfont.setWordSpacing( f.wordSpacing() );
209  styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
210  f = styledfont;
211 
212  return true;
213  }
214 
215  return false;
216 }
217 
219 {
220  return QStringLiteral( "QGIS Vera Sans" );
221 }
222 
223 bool QgsFontUtils::loadStandardTestFonts( const QStringList &loadstyles )
224 {
225  // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
226  bool fontsLoaded = false;
227 
228  QString fontFamily = standardTestFontFamily();
229  QMap<QString, QString> fontStyles;
230  fontStyles.insert( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
231  fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
232  fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
233  fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
234 
235  QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
236  for ( ; f != fontStyles.constEnd(); ++f )
237  {
238  QString fontstyle( f.key() );
239  QString fontpath( f.value() );
240  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
241  {
242  continue;
243  }
244 
245  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
246  {
247  QgsDebugMsg( QString( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ) );
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.io/browse/QTBUG-30917
257  // https://bugreports.qt.io/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' %3 from filesystem [%4]" )
263  .arg( fontFamily, fontstyle, 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) %3 from testdata.qrc" )
277  .arg( fontFamily, fontstyle, 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( QLatin1String( "Bold" ) ) );
317  f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "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( QStringLiteral( "description" ), font.toString() );
326  fontElem.setAttribute( QStringLiteral( "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( QStringLiteral( "description" ) ) );
338  if ( element.hasAttribute( QStringLiteral( "style" ) ) )
339  {
340  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "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 
365 QMimeData *QgsFontUtils::toMimeData( const QFont &font )
366 {
367  std::unique_ptr< QMimeData >mimeData( new QMimeData );
368 
369  QDomDocument fontDoc;
370  QDomElement fontElem = toXmlElement( font, fontDoc, QStringLiteral( "font" ) );
371  fontDoc.appendChild( fontElem );
372  mimeData->setText( fontDoc.toString() );
373 
374  return mimeData.release();
375 }
376 
377 QFont QgsFontUtils::fromMimeData( const QMimeData *data, bool *ok )
378 {
379  QFont font;
380  if ( ok )
381  *ok = false;
382 
383  if ( !data )
384  return font;
385 
386  QString text = data->text();
387  if ( !text.isEmpty() )
388  {
389  QDomDocument doc;
390  QDomElement elem;
391 
392  if ( doc.setContent( text ) )
393  {
394  elem = doc.documentElement();
395 
396  if ( elem.nodeName() != QStringLiteral( "font" ) )
397  elem = elem.firstChildElement( QStringLiteral( "font" ) );
398 
399  if ( setFromXmlElement( font, elem ) )
400  {
401  if ( ok )
402  *ok = true;
403  }
404  return font;
405  }
406  }
407  return font;
408 }
409 
410 static QMap<QString, QString> createTranslatedStyleMap()
411 {
412  QMap<QString, QString> translatedStyleMap;
413  QStringList words = QStringList() << QStringLiteral( "Normal" ) << QStringLiteral( "Light" ) << QStringLiteral( "Bold" ) << QStringLiteral( "Black" ) << QStringLiteral( "Demi" ) << QStringLiteral( "Italic" ) << QStringLiteral( "Oblique" );
414  Q_FOREACH ( const QString &word, words )
415  {
416  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
417  }
418  return translatedStyleMap;
419 }
420 
421 QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
422 {
423  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
424  for ( int i = 0, n = words.length(); i < n; ++i )
425  {
426  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toUtf8(), nullptr, QCoreApplication::UnicodeUTF8 );
427  }
428  return words.join( QStringLiteral( " " ) );
429 }
430 
431 QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
432 {
433  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
434  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
435  for ( int i = 0, n = words.length(); i < n; ++i )
436  {
437  if ( translatedStyleMap.contains( words[i] ) )
438  {
439  words[i] = translatedStyleMap.value( words[i] );
440  }
441  else
442  {
443  QgsDebugMsg( QString( "Warning: style map does not contain %1" ).arg( words[i] ) );
444  }
445  }
446  return words.join( QStringLiteral( " " ) );
447 }
448 
449 QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
450 {
451  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
452 
453  //style
454  css += QLatin1String( "font-style: " );
455  switch ( font.style() )
456  {
457  case QFont::StyleNormal:
458  css += QLatin1String( "normal" );
459  break;
460  case QFont::StyleItalic:
461  css += QLatin1String( "italic" );
462  break;
463  case QFont::StyleOblique:
464  css += QLatin1String( "oblique" );
465  break;
466  }
467  css += ';';
468 
469  //weight
470  int cssWeight = 400;
471  switch ( font.weight() )
472  {
473  case QFont::Light:
474  cssWeight = 300;
475  break;
476  case QFont::Normal:
477  cssWeight = 400;
478  break;
479  case QFont::DemiBold:
480  cssWeight = 600;
481  break;
482  case QFont::Bold:
483  cssWeight = 700;
484  break;
485  case QFont::Black:
486  cssWeight = 900;
487  break;
488 #if QT_VERSION >= 0x050500
489  case QFont::Thin:
490  cssWeight = 100;
491  break;
492  case QFont::ExtraLight:
493  cssWeight = 200;
494  break;
495  case QFont::Medium:
496  cssWeight = 500;
497  break;
498  case QFont::ExtraBold:
499  cssWeight = 800;
500  break;
501 #endif
502  }
503  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
504 
505  //size
506  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
507 
508  return css;
509 }
510 
511 void QgsFontUtils::addRecentFontFamily( const QString &family )
512 {
513  if ( family.isEmpty() )
514  {
515  return;
516  }
517 
518  QgsSettings settings;
519  QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
520 
521  //remove matching families
522  recentFamilies.removeAll( family );
523 
524  //then add to start of list
525  recentFamilies.prepend( family );
526 
527  //trim to 10 fonts
528  recentFamilies = recentFamilies.mid( 0, 10 );
529 
530  settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
531 }
532 
534 {
535  QgsSettings settings;
536  return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
537 }
static QFont fromMimeData(const QMimeData *data, bool *ok=nullptr)
Attempts to parse the provided mime data as a QFont.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:251
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.
static QString asCSS(const QFont &font, double pointToPixelMultiplier=1.0)
Returns a CSS string representing the specified font as closely as possible.
static QStringList recentFontFamilies()
Returns a list of recently used font families.
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)
Gets standard test font with specific style.
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()
Gets standard test font family.
static void addRecentFontFamily(const QString &family)
Adds a font family to the list of recently used font families.
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.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
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.
static QMimeData * toMimeData(const QFont &font)
Returns new mime data representing the specified font settings.