QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 #include "qgis.h"
22 
23 #include <QApplication>
24 #include <QFile>
25 #include <QFont>
26 #include <QFontDatabase>
27 #include <QFontInfo>
28 #include <QStringList>
29 #include <QMimeData>
30 #include <memory>
31 
32 bool QgsFontUtils::fontMatchOnSystem( const QFont &f )
33 {
34  QFontInfo fi = QFontInfo( f );
35  return fi.exactMatch();
36 }
37 
38 bool QgsFontUtils::fontFamilyOnSystem( const QString &family )
39 {
40  QFont tmpFont = QFont( family );
41  // compare just beginning of family string in case 'family [foundry]' differs
42  return tmpFont.family().startsWith( family, Qt::CaseInsensitive );
43 }
44 
45 bool QgsFontUtils::fontFamilyHasStyle( const QString &family, const QString &style )
46 {
47  QFontDatabase fontDB;
48  if ( !fontFamilyOnSystem( family ) )
49  return false;
50 
51  if ( fontDB.styles( family ).contains( style ) )
52  return true;
53 
54 #ifdef Q_OS_WIN
55  QString modified( style );
56  if ( style == "Roman" )
57  modified = "Normal";
58  if ( style == "Oblique" )
59  modified = "Italic";
60  if ( style == "Bold Oblique" )
61  modified = "Bold Italic";
62  if ( fontDB.styles( family ).contains( modified ) )
63  return true;
64 #endif
65 
66  return false;
67 }
68 
69 bool QgsFontUtils::fontFamilyMatchOnSystem( const QString &family, QString *chosen, bool *match )
70 {
71  QFontDatabase fontDB;
72  QStringList fontFamilies = fontDB.families();
73  bool found = false;
74 
75  QList<QString>::const_iterator it = fontFamilies.constBegin();
76  for ( ; it != fontFamilies.constEnd(); ++it )
77  {
78  // first compare just beginning of 'family [foundry]' string
79  if ( it->startsWith( family, Qt::CaseInsensitive ) )
80  {
81  found = true;
82  // keep looking if match info is requested
83  if ( match )
84  {
85  // full 'family [foundry]' strings have to match
86  *match = ( *it == family );
87  if ( *match )
88  break;
89  }
90  else
91  {
92  break;
93  }
94  }
95  }
96 
97  if ( found )
98  {
99  if ( chosen )
100  {
101  // retrieve the family actually assigned by matching algorithm
102  QFont f = QFont( family );
103  *chosen = f.family();
104  }
105  }
106  else
107  {
108  if ( chosen )
109  {
110  *chosen = QString();
111  }
112 
113  if ( match )
114  {
115  *match = false;
116  }
117  }
118 
119  return found;
120 }
121 
122 bool QgsFontUtils::updateFontViaStyle( QFont &f, const QString &fontstyle, bool fallback )
123 {
124  if ( fontstyle.isEmpty() )
125  {
126  return false;
127  }
128 
129  QFontDatabase fontDB;
130 
131  if ( !fallback )
132  {
133  // does the font even have the requested style?
134  bool hasstyle = fontFamilyHasStyle( f.family(), fontstyle );
135  if ( !hasstyle )
136  {
137  return false;
138  }
139  }
140 
141  // is the font's style already the same as requested?
142  if ( fontstyle == fontDB.styleString( f ) )
143  {
144  return false;
145  }
146 
147  QFont appfont = QApplication::font();
148  int defaultSize = appfont.pointSize(); // QFontDatabase::font() needs an integer for size
149 
150  QFont styledfont;
151  bool foundmatch = false;
152 
153  // if fontDB.font() fails, it returns the default app font; but, that may be the target style
154  styledfont = fontDB.font( f.family(), fontstyle, defaultSize );
155  if ( appfont != styledfont || fontstyle != fontDB.styleString( f ) )
156  {
157  foundmatch = true;
158  }
159 
160  // default to first found style if requested style is unavailable
161  // this helps in the situations where the passed-in font has to have a named style applied
162  if ( fallback && !foundmatch )
163  {
164  QFont testFont = QFont( f );
165  testFont.setPointSize( defaultSize );
166 
167  // prefer a style that mostly matches the passed-in font
168  const auto constFamily = fontDB.styles( f.family() );
169  for ( const QString &style : constFamily )
170  {
171  styledfont = fontDB.font( f.family(), style, defaultSize );
172  styledfont = styledfont.resolve( f );
173  if ( testFont.toString() == styledfont.toString() )
174  {
175  foundmatch = true;
176  break;
177  }
178  }
179 
180  // fallback to first style found that works
181  if ( !foundmatch )
182  {
183  const auto constFamily = fontDB.styles( f.family() );
184  for ( const QString &style : constFamily )
185  {
186  styledfont = fontDB.font( f.family(), style, defaultSize );
187  if ( QApplication::font() != styledfont )
188  {
189  foundmatch = true;
190  break;
191  }
192  }
193  }
194  }
195 
196  // similar to QFont::resolve, but font may already have pixel size set
197  // and we want to make sure that's preserved
198  if ( foundmatch )
199  {
200  if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
201  {
202  styledfont.setPointSizeF( f.pointSizeF() );
203  }
204  else if ( f.pixelSize() != -1 )
205  {
206  styledfont.setPixelSize( f.pixelSize() );
207  }
208  styledfont.setCapitalization( f.capitalization() );
209  styledfont.setUnderline( f.underline() );
210  styledfont.setStrikeOut( f.strikeOut() );
211  styledfont.setWordSpacing( f.wordSpacing() );
212  styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
213  f = styledfont;
214 
215  return true;
216  }
217 
218  return false;
219 }
220 
222 {
223  return QStringLiteral( "QGIS Vera Sans" );
224 }
225 
226 bool QgsFontUtils::loadStandardTestFonts( const QStringList &loadstyles )
227 {
228  // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
229  bool fontsLoaded = false;
230 
231  QString fontFamily = standardTestFontFamily();
232  QMap<QString, QString> fontStyles;
233  fontStyles.insert( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
234  fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
235  fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
236  fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
237 
238  QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
239  for ( ; f != fontStyles.constEnd(); ++f )
240  {
241  QString fontstyle( f.key() );
242  QString fontpath( f.value() );
243  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
244  {
245  continue;
246  }
247 
248  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
249  {
250  QgsDebugMsg( QStringLiteral( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ) );
251  }
252  else
253  {
254  bool loaded = false;
256  {
257  // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
258  // from qrc resources load but fail to work and default font is substituted [LS]:
259  // https://bugreports.qt.io/browse/QTBUG-30917
260  // https://bugreports.qt.io/browse/QTBUG-32789
261  QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
262  int fontID = QFontDatabase::addApplicationFont( fontPath );
263  loaded = ( fontID != -1 );
264  fontsLoaded = ( fontsLoaded || loaded );
265  QgsDebugMsg( QStringLiteral( "Test font '%1 %2' %3 from filesystem [%4]" )
266  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ) );
267  QFontDatabase db;
268  QgsDebugMsg( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ) );
269  }
270  else
271  {
272  QFile fontResource( ":/testdata/font/" + fontpath );
273  if ( fontResource.open( QIODevice::ReadOnly ) )
274  {
275  int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
276  loaded = ( fontID != -1 );
277  fontsLoaded = ( fontsLoaded || loaded );
278  }
279  QgsDebugMsg( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
280  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ) );
281  }
282  }
283  }
284 
285  return fontsLoaded;
286 }
287 
288 QFont QgsFontUtils::getStandardTestFont( const QString &style, int pointsize )
289 {
290  if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
291  {
292  loadStandardTestFonts( QStringList() << style );
293  }
294 
295  QFontDatabase fontDB;
296  QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
297 #ifdef Q_OS_WIN
298  if ( !f.exactMatch() )
299  {
300  QString modified;
301  if ( style == "Roman" )
302  modified = "Normal";
303  else if ( style == "Oblique" )
304  modified = "Italic";
305  else if ( style == "Bold Oblique" )
306  modified = "Bold Italic";
307  if ( !modified.isEmpty() )
308  f = fontDB.font( standardTestFontFamily(), modified, pointsize );
309  }
310  if ( !f.exactMatch() )
311  {
312  QgsDebugMsg( QStringLiteral( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
313  QgsDebugMsg( QStringLiteral( "Requested: %1" ).arg( f.toString() ) );
314  QFontInfo fi( f );
315  QgsDebugMsg( QStringLiteral( "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() ) );
316  }
317 #endif
318  // in case above statement fails to set style
319  f.setBold( style.contains( QLatin1String( "Bold" ) ) );
320  f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "Italic" ) ) );
321 
322  return f;
323 }
324 
325 QDomElement QgsFontUtils::toXmlElement( const QFont &font, QDomDocument &document, const QString &elementName )
326 {
327  QDomElement fontElem = document.createElement( elementName );
328  fontElem.setAttribute( QStringLiteral( "description" ), font.toString() );
329  fontElem.setAttribute( QStringLiteral( "style" ), untranslateNamedStyle( font.styleName() ) );
330  return fontElem;
331 }
332 
333 bool QgsFontUtils::setFromXmlElement( QFont &font, const QDomElement &element )
334 {
335  if ( element.isNull() )
336  {
337  return false;
338  }
339 
340  font.fromString( element.attribute( QStringLiteral( "description" ) ) );
341  if ( element.hasAttribute( QStringLiteral( "style" ) ) )
342  {
343  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "style" ) ) ) );
344  }
345 
346  return true;
347 }
348 
349 bool QgsFontUtils::setFromXmlChildNode( QFont &font, const QDomElement &element, const QString &childNode )
350 {
351  if ( element.isNull() )
352  {
353  return false;
354  }
355 
356  QDomNodeList nodeList = element.elementsByTagName( childNode );
357  if ( !nodeList.isEmpty() )
358  {
359  QDomElement fontElem = nodeList.at( 0 ).toElement();
360  return setFromXmlElement( font, fontElem );
361  }
362  else
363  {
364  return false;
365  }
366 }
367 
368 QMimeData *QgsFontUtils::toMimeData( const QFont &font )
369 {
370  std::unique_ptr< QMimeData >mimeData( new QMimeData );
371 
372  QDomDocument fontDoc;
373  QDomElement fontElem = toXmlElement( font, fontDoc, QStringLiteral( "font" ) );
374  fontDoc.appendChild( fontElem );
375  mimeData->setText( fontDoc.toString() );
376 
377  return mimeData.release();
378 }
379 
380 QFont QgsFontUtils::fromMimeData( const QMimeData *data, bool *ok )
381 {
382  QFont font;
383  if ( ok )
384  *ok = false;
385 
386  if ( !data )
387  return font;
388 
389  QString text = data->text();
390  if ( !text.isEmpty() )
391  {
392  QDomDocument doc;
393  QDomElement elem;
394 
395  if ( doc.setContent( text ) )
396  {
397  elem = doc.documentElement();
398 
399  if ( elem.nodeName() != QStringLiteral( "font" ) )
400  elem = elem.firstChildElement( QStringLiteral( "font" ) );
401 
402  if ( setFromXmlElement( font, elem ) )
403  {
404  if ( ok )
405  *ok = true;
406  }
407  return font;
408  }
409  }
410  return font;
411 }
412 
413 static QMap<QString, QString> createTranslatedStyleMap()
414 {
415  QMap<QString, QString> translatedStyleMap;
416  QStringList words = QStringList()
417  << QStringLiteral( "Normal" )
418  << QStringLiteral( "Regular" )
419  << QStringLiteral( "Light" )
420  << QStringLiteral( "Bold" )
421  << QStringLiteral( "Black" )
422  << QStringLiteral( "Demi" )
423  << QStringLiteral( "Italic" )
424  << QStringLiteral( "Oblique" );
425  const auto constWords = words;
426  for ( const QString &word : constWords )
427  {
428  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
429  }
430  return translatedStyleMap;
431 }
432 
433 QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
434 {
435  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
436  for ( int i = 0, n = words.length(); i < n; ++i )
437  {
438  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toUtf8(), nullptr, QCoreApplication::UnicodeUTF8 );
439  }
440  return words.join( QStringLiteral( " " ) );
441 }
442 
443 QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
444 {
445  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
446  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
447  for ( int i = 0, n = words.length(); i < n; ++i )
448  {
449  if ( translatedStyleMap.contains( words[i] ) )
450  {
451  words[i] = translatedStyleMap.value( words[i] );
452  }
453  else
454  {
455  QgsDebugMsg( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ) );
456  }
457  }
458  return words.join( QStringLiteral( " " ) );
459 }
460 
461 QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
462 {
463  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
464 
465  //style
466  css += QLatin1String( "font-style: " );
467  switch ( font.style() )
468  {
469  case QFont::StyleNormal:
470  css += QLatin1String( "normal" );
471  break;
472  case QFont::StyleItalic:
473  css += QLatin1String( "italic" );
474  break;
475  case QFont::StyleOblique:
476  css += QLatin1String( "oblique" );
477  break;
478  }
479  css += ';';
480 
481  //weight
482  int cssWeight = 400;
483  switch ( font.weight() )
484  {
485  case QFont::Light:
486  cssWeight = 300;
487  break;
488  case QFont::Normal:
489  cssWeight = 400;
490  break;
491  case QFont::DemiBold:
492  cssWeight = 600;
493  break;
494  case QFont::Bold:
495  cssWeight = 700;
496  break;
497  case QFont::Black:
498  cssWeight = 900;
499  break;
500  case QFont::Thin:
501  cssWeight = 100;
502  break;
503  case QFont::ExtraLight:
504  cssWeight = 200;
505  break;
506  case QFont::Medium:
507  cssWeight = 500;
508  break;
509  case QFont::ExtraBold:
510  cssWeight = 800;
511  break;
512  }
513  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
514 
515  //size
516  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
517 
518  return css;
519 }
520 
521 void QgsFontUtils::addRecentFontFamily( const QString &family )
522 {
523  if ( family.isEmpty() )
524  {
525  return;
526  }
527 
528  QgsSettings settings;
529  QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
530 
531  //remove matching families
532  recentFamilies.removeAll( family );
533 
534  //then add to start of list
535  recentFamilies.prepend( family );
536 
537  //trim to 10 fonts
538  recentFamilies = recentFamilies.mid( 0, 10 );
539 
540  settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
541 }
542 
544 {
545  QgsSettings settings;
546  return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
547 }
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:265
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.