QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
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  for ( const QString &style : constFamily )
184  {
185  styledfont = fontDB.font( f.family(), style, defaultSize );
186  if ( QApplication::font() != styledfont )
187  {
188  foundmatch = true;
189  break;
190  }
191  }
192  }
193  }
194 
195  // similar to QFont::resolve, but font may already have pixel size set
196  // and we want to make sure that's preserved
197  if ( foundmatch )
198  {
199  if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
200  {
201  styledfont.setPointSizeF( f.pointSizeF() );
202  }
203  else if ( f.pixelSize() != -1 )
204  {
205  styledfont.setPixelSize( f.pixelSize() );
206  }
207  styledfont.setCapitalization( f.capitalization() );
208  styledfont.setUnderline( f.underline() );
209  styledfont.setStrikeOut( f.strikeOut() );
210  styledfont.setWordSpacing( f.wordSpacing() );
211  styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
212  f = styledfont;
213 
214  return true;
215  }
216 
217  return false;
218 }
219 
221 {
222  return QStringLiteral( "QGIS Vera Sans" );
223 }
224 
225 bool QgsFontUtils::loadStandardTestFonts( const QStringList &loadstyles )
226 {
227  // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
228  bool fontsLoaded = false;
229 
230  QString fontFamily = standardTestFontFamily();
231  QMap<QString, QString> fontStyles;
232  fontStyles.insert( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
233  fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
234  fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
235  fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
236 
237  QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
238  for ( ; f != fontStyles.constEnd(); ++f )
239  {
240  QString fontstyle( f.key() );
241  QString fontpath( f.value() );
242  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
243  {
244  continue;
245  }
246 
247  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
248  {
249  QgsDebugMsgLevel( QStringLiteral( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ), 2 );
250  }
251  else
252  {
253  bool loaded = false;
255  {
256  // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
257  // from qrc resources load but fail to work and default font is substituted [LS]:
258  // https://bugreports.qt.io/browse/QTBUG-30917
259  // https://bugreports.qt.io/browse/QTBUG-32789
260  QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
261  int fontID = QFontDatabase::addApplicationFont( fontPath );
262  loaded = ( fontID != -1 );
263  fontsLoaded = ( fontsLoaded || loaded );
264  QgsDebugMsgLevel( QStringLiteral( "Test font '%1 %2' %3 from filesystem [%4]" )
265  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ), 2 );
266  QFontDatabase db;
267  QgsDebugMsgLevel( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ), 2 );
268  }
269  else
270  {
271  QFile fontResource( ":/testdata/font/" + fontpath );
272  if ( fontResource.open( QIODevice::ReadOnly ) )
273  {
274  int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
275  loaded = ( fontID != -1 );
276  fontsLoaded = ( fontsLoaded || loaded );
277  }
278  QgsDebugMsgLevel( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
279  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ), 2 );
280  }
281  }
282  }
283 
284  return fontsLoaded;
285 }
286 
287 QFont QgsFontUtils::getStandardTestFont( const QString &style, int pointsize )
288 {
289  if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
290  {
291  loadStandardTestFonts( QStringList() << style );
292  }
293 
294  QFontDatabase fontDB;
295  QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
296 #ifdef Q_OS_WIN
297  if ( !f.exactMatch() )
298  {
299  QString modified;
300  if ( style == "Roman" )
301  modified = "Normal";
302  else if ( style == "Oblique" )
303  modified = "Italic";
304  else if ( style == "Bold Oblique" )
305  modified = "Bold Italic";
306  if ( !modified.isEmpty() )
307  f = fontDB.font( standardTestFontFamily(), modified, pointsize );
308  }
309  if ( !f.exactMatch() )
310  {
311  QgsDebugMsg( QStringLiteral( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
312  QgsDebugMsg( QStringLiteral( "Requested: %1" ).arg( f.toString() ) );
313  QFontInfo fi( f );
314  QgsDebugMsg( QStringLiteral( "Replaced: %1,%2,%3,%4,%5,%6,%7,%8,%9" ).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() ) );
315  }
316 #endif
317  // in case above statement fails to set style
318  f.setBold( style.contains( QLatin1String( "Bold" ) ) );
319  f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "Italic" ) ) );
320 
321  return f;
322 }
323 
324 QDomElement QgsFontUtils::toXmlElement( const QFont &font, QDomDocument &document, const QString &elementName )
325 {
326  QDomElement fontElem = document.createElement( elementName );
327  fontElem.setAttribute( QStringLiteral( "description" ), font.toString() );
328  fontElem.setAttribute( QStringLiteral( "style" ), untranslateNamedStyle( font.styleName() ) );
329  return fontElem;
330 }
331 
332 bool QgsFontUtils::setFromXmlElement( QFont &font, const QDomElement &element )
333 {
334  if ( element.isNull() )
335  {
336  return false;
337  }
338 
339  font.fromString( element.attribute( QStringLiteral( "description" ) ) );
340  if ( element.hasAttribute( QStringLiteral( "style" ) ) )
341  {
342  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "style" ) ) ) );
343  }
344 
345  return true;
346 }
347 
348 bool QgsFontUtils::setFromXmlChildNode( QFont &font, const QDomElement &element, const QString &childNode )
349 {
350  if ( element.isNull() )
351  {
352  return false;
353  }
354 
355  QDomNodeList nodeList = element.elementsByTagName( childNode );
356  if ( !nodeList.isEmpty() )
357  {
358  QDomElement fontElem = nodeList.at( 0 ).toElement();
359  return setFromXmlElement( font, fontElem );
360  }
361  else
362  {
363  return false;
364  }
365 }
366 
367 QMimeData *QgsFontUtils::toMimeData( const QFont &font )
368 {
369  std::unique_ptr< QMimeData >mimeData( new QMimeData );
370 
371  QDomDocument fontDoc;
372  QDomElement fontElem = toXmlElement( font, fontDoc, QStringLiteral( "font" ) );
373  fontDoc.appendChild( fontElem );
374  mimeData->setText( fontDoc.toString() );
375 
376  return mimeData.release();
377 }
378 
379 QFont QgsFontUtils::fromMimeData( const QMimeData *data, bool *ok )
380 {
381  QFont font;
382  if ( ok )
383  *ok = false;
384 
385  if ( !data )
386  return font;
387 
388  QString text = data->text();
389  if ( !text.isEmpty() )
390  {
391  QDomDocument doc;
392  QDomElement elem;
393 
394  if ( doc.setContent( text ) )
395  {
396  elem = doc.documentElement();
397 
398  if ( elem.nodeName() != QLatin1String( "font" ) )
399  elem = elem.firstChildElement( QStringLiteral( "font" ) );
400 
401  if ( setFromXmlElement( font, elem ) )
402  {
403  if ( ok )
404  *ok = true;
405  }
406  return font;
407  }
408  }
409  return font;
410 }
411 
412 static QMap<QString, QString> createTranslatedStyleMap()
413 {
414  QMap<QString, QString> translatedStyleMap;
415  QStringList words = QStringList()
416  << QStringLiteral( "Normal" )
417  << QStringLiteral( "Regular" )
418  << QStringLiteral( "Light" )
419  << QStringLiteral( "Bold" )
420  << QStringLiteral( "Black" )
421  << QStringLiteral( "Demi" )
422  << QStringLiteral( "Italic" )
423  << QStringLiteral( "Oblique" );
424  const auto constWords = words;
425  for ( const QString &word : constWords )
426  {
427  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
428  }
429  return translatedStyleMap;
430 }
431 
432 QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
433 {
434 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
435  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
436 #else
437  QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
438 #endif
439  for ( int i = 0, n = words.length(); i < n; ++i )
440  {
441  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toLocal8Bit().constData() );
442  }
443  return words.join( QLatin1Char( ' ' ) );
444 }
445 
446 QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
447 {
448  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
449 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
450  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
451 #else
452  QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
453 #endif
454 
455  for ( int i = 0, n = words.length(); i < n; ++i )
456  {
457  if ( translatedStyleMap.contains( words[i] ) )
458  {
459  words[i] = translatedStyleMap.value( words[i] );
460  }
461  else
462  {
463  QgsDebugMsgLevel( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ), 2 );
464  }
465  }
466  return words.join( QLatin1Char( ' ' ) );
467 }
468 
469 QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
470 {
471  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
472 
473  //style
474  css += QLatin1String( "font-style: " );
475  switch ( font.style() )
476  {
477  case QFont::StyleNormal:
478  css += QLatin1String( "normal" );
479  break;
480  case QFont::StyleItalic:
481  css += QLatin1String( "italic" );
482  break;
483  case QFont::StyleOblique:
484  css += QLatin1String( "oblique" );
485  break;
486  }
487  css += ';';
488 
489  //weight
490  int cssWeight = 400;
491  switch ( font.weight() )
492  {
493  case QFont::Light:
494  cssWeight = 300;
495  break;
496  case QFont::Normal:
497  cssWeight = 400;
498  break;
499  case QFont::DemiBold:
500  cssWeight = 600;
501  break;
502  case QFont::Bold:
503  cssWeight = 700;
504  break;
505  case QFont::Black:
506  cssWeight = 900;
507  break;
508  case QFont::Thin:
509  cssWeight = 100;
510  break;
511  case QFont::ExtraLight:
512  cssWeight = 200;
513  break;
514  case QFont::Medium:
515  cssWeight = 500;
516  break;
517  case QFont::ExtraBold:
518  cssWeight = 800;
519  break;
520  }
521  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
522 
523  //size
524  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
525 
526  return css;
527 }
528 
529 void QgsFontUtils::addRecentFontFamily( const QString &family )
530 {
531  if ( family.isEmpty() )
532  {
533  return;
534  }
535 
536  QgsSettings settings;
537  QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
538 
539  //remove matching families
540  recentFamilies.removeAll( family );
541 
542  //then add to start of list
543  recentFamilies.prepend( family );
544 
545  //trim to 10 fonts
546  recentFamilies = recentFamilies.mid( 0, 10 );
547 
548  settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
549 }
550 
552 {
553  QgsSettings settings;
554  return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
555 }
QgsFontUtils::setFromXmlElement
static bool setFromXmlElement(QFont &font, const QDomElement &element)
Sets the properties of a font to match the properties stored in an XML element.
Definition: qgsfontutils.cpp:332
QgsFontUtils::toMimeData
static QMimeData * toMimeData(const QFont &font)
Returns new mime data representing the specified font settings.
Definition: qgsfontutils.cpp:367
QgsSettings::value
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Definition: qgssettings.cpp:174
QgsFontUtils::asCSS
static QString asCSS(const QFont &font, double pointToPixelMultiplier=1.0)
Returns a CSS string representing the specified font as closely as possible.
Definition: qgsfontutils.cpp:469
QgsDebugMsgLevel
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
QgsFontUtils::fromMimeData
static QFont fromMimeData(const QMimeData *data, bool *ok=nullptr)
Attempts to parse the provided mime data as a QFont.
Definition: qgsfontutils.cpp:379
qgis.h
QgsSettings
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QgsFontUtils::recentFontFamilies
static QStringList recentFontFamilies()
Returns a list of recently used font families.
Definition: qgsfontutils.cpp:551
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
qgsfontutils.h
QgsFontUtils::getStandardTestFont
static QFont getStandardTestFont(const QString &style="Roman", int pointsize=12)
Gets standard test font with specific style.
Definition: qgsfontutils.cpp:287
QgsFontUtils::standardTestFontFamily
static QString standardTestFontFamily()
Gets standard test font family.
Definition: qgsfontutils.cpp:220
qgsapplication.h
qgsDoubleNear
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:315
QgsFontUtils::translateNamedStyle
static QString translateNamedStyle(const QString &namedStyle)
Returns the localized named style of a font, if such a translation is available.
Definition: qgsfontutils.cpp:432
QgsFontUtils::fontFamilyMatchOnSystem
static bool fontFamilyMatchOnSystem(const QString &family, QString *chosen=nullptr, bool *match=nullptr)
Check whether font family is on system.
Definition: qgsfontutils.cpp:69
QgsSettings::setValue
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
Definition: qgssettings.cpp:289
QgsApplication::buildSourcePath
static QString buildSourcePath()
Returns path to the source directory. Valid only when running from build directory.
Definition: qgsapplication.cpp:1647
QgsFontUtils::fontFamilyOnSystem
static bool fontFamilyOnSystem(const QString &family)
Check whether font family is on system in a quick manner, which does not compare [foundry].
Definition: qgsfontutils.cpp:38
QgsApplication::isRunningFromBuildDir
static bool isRunningFromBuildDir()
Indicates whether running from build directory (not installed)
Definition: qgsapplication.h:530
QgsFontUtils::fontMatchOnSystem
static bool fontMatchOnSystem(const QFont &f)
Check whether exact font is on system.
Definition: qgsfontutils.cpp:32
QgsFontUtils::setFromXmlChildNode
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.
Definition: qgsfontutils.cpp:348
QgsFontUtils::loadStandardTestFonts
static bool loadStandardTestFonts(const QStringList &loadstyles)
Loads standard test fonts from filesystem or qrc resource.
Definition: qgsfontutils.cpp:225
QgsFontUtils::toXmlElement
static QDomElement toXmlElement(const QFont &font, QDomDocument &document, const QString &elementName)
Returns a DOM element containing the properties of the font.
Definition: qgsfontutils.cpp:324
qgssettings.h
QgsFontUtils::addRecentFontFamily
static void addRecentFontFamily(const QString &family)
Adds a font family to the list of recently used font families.
Definition: qgsfontutils.cpp:529
qgslogger.h
QgsFontUtils::untranslateNamedStyle
static QString untranslateNamedStyle(const QString &namedStyle)
Returns the english named style of a font, if possible.
Definition: qgsfontutils.cpp:446
QgsFontUtils::updateFontViaStyle
static bool updateFontViaStyle(QFont &f, const QString &fontstyle, bool fallback=false)
Updates font with named style and retain all font properties.
Definition: qgsfontutils.cpp:122
QgsFontUtils::fontFamilyHasStyle
static bool fontFamilyHasStyle(const QString &family, const QString &style)
Check whether font family on system has specific style.
Definition: qgsfontutils.cpp:45