QGIS API Documentation  3.20.0-Odense (decaadbb31)
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 }
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:598
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38