QGIS API Documentation  3.6.0-Noosa (5873452)
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  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
169  {
170  styledfont = fontDB.font( f.family(), style, defaultSize );
171  styledfont = styledfont.resolve( f );
172  if ( testFont.toString() == styledfont.toString() )
173  {
174  foundmatch = true;
175  break;
176  }
177  }
178 
179  // fallback to first style found that works
180  if ( !foundmatch )
181  {
182  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
183  {
184  styledfont = fontDB.font( f.family(), style, defaultSize );
185  if ( QApplication::font() != styledfont )
186  {
187  foundmatch = true;
188  break;
189  }
190  }
191  }
192  }
193 
194  // similar to QFont::resolve, but font may already have pixel size set
195  // and we want to make sure that's preserved
196  if ( foundmatch )
197  {
198  if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
199  {
200  styledfont.setPointSizeF( f.pointSizeF() );
201  }
202  else if ( f.pixelSize() != -1 )
203  {
204  styledfont.setPixelSize( f.pixelSize() );
205  }
206  styledfont.setCapitalization( f.capitalization() );
207  styledfont.setUnderline( f.underline() );
208  styledfont.setStrikeOut( f.strikeOut() );
209  styledfont.setWordSpacing( f.wordSpacing() );
210  styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
211  f = styledfont;
212 
213  return true;
214  }
215 
216  return false;
217 }
218 
220 {
221  return QStringLiteral( "QGIS Vera Sans" );
222 }
223 
224 bool QgsFontUtils::loadStandardTestFonts( const QStringList &loadstyles )
225 {
226  // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
227  bool fontsLoaded = false;
228 
229  QString fontFamily = standardTestFontFamily();
230  QMap<QString, QString> fontStyles;
231  fontStyles.insert( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
232  fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
233  fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
234  fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
235 
236  QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
237  for ( ; f != fontStyles.constEnd(); ++f )
238  {
239  QString fontstyle( f.key() );
240  QString fontpath( f.value() );
241  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
242  {
243  continue;
244  }
245 
246  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
247  {
248  QgsDebugMsg( QStringLiteral( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ) );
249  }
250  else
251  {
252  bool loaded = false;
254  {
255  // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
256  // from qrc resources load but fail to work and default font is substituted [LS]:
257  // https://bugreports.qt.io/browse/QTBUG-30917
258  // https://bugreports.qt.io/browse/QTBUG-32789
259  QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
260  int fontID = QFontDatabase::addApplicationFont( fontPath );
261  loaded = ( fontID != -1 );
262  fontsLoaded = ( fontsLoaded || loaded );
263  QgsDebugMsg( QStringLiteral( "Test font '%1 %2' %3 from filesystem [%4]" )
264  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ) );
265  QFontDatabase db;
266  QgsDebugMsg( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ) );
267  }
268  else
269  {
270  QFile fontResource( ":/testdata/font/" + fontpath );
271  if ( fontResource.open( QIODevice::ReadOnly ) )
272  {
273  int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
274  loaded = ( fontID != -1 );
275  fontsLoaded = ( fontsLoaded || loaded );
276  }
277  QgsDebugMsg( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
278  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ) );
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  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,%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() ) );
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  QDomNodeList nodeList = element.elementsByTagName( childNode );
355  if ( !nodeList.isEmpty() )
356  {
357  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  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  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() != QStringLiteral( "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  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  Q_FOREACH ( const QString &word, words )
424  {
425  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
426  }
427  return translatedStyleMap;
428 }
429 
430 QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
431 {
432  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
433  for ( int i = 0, n = words.length(); i < n; ++i )
434  {
435  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toUtf8(), nullptr, QCoreApplication::UnicodeUTF8 );
436  }
437  return words.join( QStringLiteral( " " ) );
438 }
439 
440 QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
441 {
442  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
443  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
444  for ( int i = 0, n = words.length(); i < n; ++i )
445  {
446  if ( translatedStyleMap.contains( words[i] ) )
447  {
448  words[i] = translatedStyleMap.value( words[i] );
449  }
450  else
451  {
452  QgsDebugMsg( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ) );
453  }
454  }
455  return words.join( QStringLiteral( " " ) );
456 }
457 
458 QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
459 {
460  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
461 
462  //style
463  css += QLatin1String( "font-style: " );
464  switch ( font.style() )
465  {
466  case QFont::StyleNormal:
467  css += QLatin1String( "normal" );
468  break;
469  case QFont::StyleItalic:
470  css += QLatin1String( "italic" );
471  break;
472  case QFont::StyleOblique:
473  css += QLatin1String( "oblique" );
474  break;
475  }
476  css += ';';
477 
478  //weight
479  int cssWeight = 400;
480  switch ( font.weight() )
481  {
482  case QFont::Light:
483  cssWeight = 300;
484  break;
485  case QFont::Normal:
486  cssWeight = 400;
487  break;
488  case QFont::DemiBold:
489  cssWeight = 600;
490  break;
491  case QFont::Bold:
492  cssWeight = 700;
493  break;
494  case QFont::Black:
495  cssWeight = 900;
496  break;
497  case QFont::Thin:
498  cssWeight = 100;
499  break;
500  case QFont::ExtraLight:
501  cssWeight = 200;
502  break;
503  case QFont::Medium:
504  cssWeight = 500;
505  break;
506  case QFont::ExtraBold:
507  cssWeight = 800;
508  break;
509  }
510  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
511 
512  //size
513  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
514 
515  return css;
516 }
517 
518 void QgsFontUtils::addRecentFontFamily( const QString &family )
519 {
520  if ( family.isEmpty() )
521  {
522  return;
523  }
524 
525  QgsSettings settings;
526  QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
527 
528  //remove matching families
529  recentFamilies.removeAll( family );
530 
531  //then add to start of list
532  recentFamilies.prepend( family );
533 
534  //trim to 10 fonts
535  recentFamilies = recentFamilies.mid( 0, 10 );
536 
537  settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
538 }
539 
541 {
542  QgsSettings settings;
543  return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
544 }
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.