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