QGIS API Documentation  3.21.0-Master (5b68dc587e)
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 bool QgsFontUtils::fontFamilyMatchOnSystem( const QString &family, QString *chosen, bool *match )
70 {
71  const QFontDatabase fontDB;
72  const 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  const 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  const 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  const QFont appfont = QApplication::font();
148  const 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  const 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  const QString fontstyle( f.key() );
241  const 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  const QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
261  const 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  QgsDebugMsgLevel( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( QFontDatabase().applicationFontFamilies( fontID ).join( "," ) ), 2 );
267  }
268  else
269  {
270  QFile fontResource( ":/testdata/font/" + fontpath );
271  if ( fontResource.open( QIODevice::ReadOnly ) )
272  {
273  const int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
274  loaded = ( fontID != -1 );
275  fontsLoaded = ( fontsLoaded || loaded );
276  }
277  QgsDebugMsgLevel( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
278  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ), 2 );
279  }
280  }
281  }
282 
283  return fontsLoaded;
284 }
285 
286 QFont QgsFontUtils::getStandardTestFont( const QString &style, int pointsize )
287 {
288  if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
289  {
290  loadStandardTestFonts( QStringList() << style );
291  }
292 
293  const QFontDatabase fontDB;
294  QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
295 #ifdef Q_OS_WIN
296  if ( !f.exactMatch() )
297  {
298  QString modified;
299  if ( style == "Roman" )
300  modified = "Normal";
301  else if ( style == "Oblique" )
302  modified = "Italic";
303  else if ( style == "Bold Oblique" )
304  modified = "Bold Italic";
305  if ( !modified.isEmpty() )
306  f = fontDB.font( standardTestFontFamily(), modified, pointsize );
307  }
308  if ( !f.exactMatch() )
309  {
310  QgsDebugMsg( QStringLiteral( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
311  QgsDebugMsg( QStringLiteral( "Requested: %1" ).arg( f.toString() ) );
312  QFontInfo fi( f );
313  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() ) );
314  }
315 #endif
316  // in case above statement fails to set style
317  f.setBold( style.contains( QLatin1String( "Bold" ) ) );
318  f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "Italic" ) ) );
319 
320  return f;
321 }
322 
323 QDomElement QgsFontUtils::toXmlElement( const QFont &font, QDomDocument &document, const QString &elementName )
324 {
325  QDomElement fontElem = document.createElement( elementName );
326  fontElem.setAttribute( QStringLiteral( "description" ), font.toString() );
327  fontElem.setAttribute( QStringLiteral( "style" ), untranslateNamedStyle( font.styleName() ) );
328  return fontElem;
329 }
330 
331 bool QgsFontUtils::setFromXmlElement( QFont &font, const QDomElement &element )
332 {
333  if ( element.isNull() )
334  {
335  return false;
336  }
337 
338  font.fromString( element.attribute( QStringLiteral( "description" ) ) );
339  if ( element.hasAttribute( QStringLiteral( "style" ) ) )
340  {
341  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "style" ) ) ) );
342  }
343 
344  return true;
345 }
346 
347 bool QgsFontUtils::setFromXmlChildNode( QFont &font, const QDomElement &element, const QString &childNode )
348 {
349  if ( element.isNull() )
350  {
351  return false;
352  }
353 
354  const QDomNodeList nodeList = element.elementsByTagName( childNode );
355  if ( !nodeList.isEmpty() )
356  {
357  const QDomElement fontElem = nodeList.at( 0 ).toElement();
358  return setFromXmlElement( font, fontElem );
359  }
360  else
361  {
362  return false;
363  }
364 }
365 
366 QMimeData *QgsFontUtils::toMimeData( const QFont &font )
367 {
368  std::unique_ptr< QMimeData >mimeData( new QMimeData );
369 
370  QDomDocument fontDoc;
371  const QDomElement fontElem = toXmlElement( font, fontDoc, QStringLiteral( "font" ) );
372  fontDoc.appendChild( fontElem );
373  mimeData->setText( fontDoc.toString() );
374 
375  return mimeData.release();
376 }
377 
378 QFont QgsFontUtils::fromMimeData( const QMimeData *data, bool *ok )
379 {
380  QFont font;
381  if ( ok )
382  *ok = false;
383 
384  if ( !data )
385  return font;
386 
387  const QString text = data->text();
388  if ( !text.isEmpty() )
389  {
390  QDomDocument doc;
391  QDomElement elem;
392 
393  if ( doc.setContent( text ) )
394  {
395  elem = doc.documentElement();
396 
397  if ( elem.nodeName() != QLatin1String( "font" ) )
398  elem = elem.firstChildElement( QStringLiteral( "font" ) );
399 
400  if ( setFromXmlElement( font, elem ) )
401  {
402  if ( ok )
403  *ok = true;
404  }
405  return font;
406  }
407  }
408  return font;
409 }
410 
411 static QMap<QString, QString> createTranslatedStyleMap()
412 {
413  QMap<QString, QString> translatedStyleMap;
414  const QStringList words = QStringList()
415  << QStringLiteral( "Normal" )
416  << QStringLiteral( "Regular" )
417  << QStringLiteral( "Light" )
418  << QStringLiteral( "Bold" )
419  << QStringLiteral( "Black" )
420  << QStringLiteral( "Demi" )
421  << QStringLiteral( "Italic" )
422  << QStringLiteral( "Oblique" );
423  const auto constWords = words;
424  for ( const QString &word : constWords )
425  {
426  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
427  }
428  return translatedStyleMap;
429 }
430 
431 QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
432 {
433 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
434  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
435 #else
436  QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
437 #endif
438  for ( int i = 0, n = words.length(); i < n; ++i )
439  {
440  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toLocal8Bit().constData() );
441  }
442  return words.join( QLatin1Char( ' ' ) );
443 }
444 
445 QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
446 {
447  static const QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
448 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
449  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
450 #else
451  QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
452 #endif
453 
454  for ( int i = 0, n = words.length(); i < n; ++i )
455  {
456  if ( translatedStyleMap.contains( words[i] ) )
457  {
458  words[i] = translatedStyleMap.value( words[i] );
459  }
460  else
461  {
462  QgsDebugMsgLevel( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ), 2 );
463  }
464  }
465  return words.join( QLatin1Char( ' ' ) );
466 }
467 
468 QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
469 {
470  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
471 
472  //style
473  css += QLatin1String( "font-style: " );
474  switch ( font.style() )
475  {
476  case QFont::StyleNormal:
477  css += QLatin1String( "normal" );
478  break;
479  case QFont::StyleItalic:
480  css += QLatin1String( "italic" );
481  break;
482  case QFont::StyleOblique:
483  css += QLatin1String( "oblique" );
484  break;
485  }
486  css += ';';
487 
488  //weight
489  int cssWeight = 400;
490  switch ( font.weight() )
491  {
492  case QFont::Light:
493  cssWeight = 300;
494  break;
495  case QFont::Normal:
496  cssWeight = 400;
497  break;
498  case QFont::DemiBold:
499  cssWeight = 600;
500  break;
501  case QFont::Bold:
502  cssWeight = 700;
503  break;
504  case QFont::Black:
505  cssWeight = 900;
506  break;
507  case QFont::Thin:
508  cssWeight = 100;
509  break;
510  case QFont::ExtraLight:
511  cssWeight = 200;
512  break;
513  case QFont::Medium:
514  cssWeight = 500;
515  break;
516  case QFont::ExtraBold:
517  cssWeight = 800;
518  break;
519  }
520  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
521 
522  //size
523  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
524 
525  return css;
526 }
527 
528 void QgsFontUtils::addRecentFontFamily( const QString &family )
529 {
530  if ( family.isEmpty() )
531  {
532  return;
533  }
534 
535  QgsSettings settings;
536  QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
537 
538  //remove matching families
539  recentFamilies.removeAll( family );
540 
541  //then add to start of list
542  recentFamilies.prepend( family );
543 
544  //trim to 10 fonts
545  recentFamilies = recentFamilies.mid( 0, 10 );
546 
547  settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
548 }
549 
551 {
552  const QgsSettings settings;
553  return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
554 }
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 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.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1234
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38