QGIS API Documentation  3.12.1-BucureČ™ti (121cc00ff0)
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  QgsDebugMsg( QStringLiteral( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ) );
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  QgsDebugMsg( QStringLiteral( "Test font '%1 %2' %3 from filesystem [%4]" )
265  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ) );
266  QFontDatabase db;
267  QgsDebugMsg( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ) );
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  QgsDebugMsg( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
279  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ) );
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() != QStringLiteral( "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  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
435  for ( int i = 0, n = words.length(); i < n; ++i )
436  {
437  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toLocal8Bit().constData() );
438  }
439  return words.join( QStringLiteral( " " ) );
440 }
441 
442 QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
443 {
444  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
445  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
446  for ( int i = 0, n = words.length(); i < n; ++i )
447  {
448  if ( translatedStyleMap.contains( words[i] ) )
449  {
450  words[i] = translatedStyleMap.value( words[i] );
451  }
452  else
453  {
454  QgsDebugMsg( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ) );
455  }
456  }
457  return words.join( QStringLiteral( " " ) );
458 }
459 
460 QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
461 {
462  QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
463 
464  //style
465  css += QLatin1String( "font-style: " );
466  switch ( font.style() )
467  {
468  case QFont::StyleNormal:
469  css += QLatin1String( "normal" );
470  break;
471  case QFont::StyleItalic:
472  css += QLatin1String( "italic" );
473  break;
474  case QFont::StyleOblique:
475  css += QLatin1String( "oblique" );
476  break;
477  }
478  css += ';';
479 
480  //weight
481  int cssWeight = 400;
482  switch ( font.weight() )
483  {
484  case QFont::Light:
485  cssWeight = 300;
486  break;
487  case QFont::Normal:
488  cssWeight = 400;
489  break;
490  case QFont::DemiBold:
491  cssWeight = 600;
492  break;
493  case QFont::Bold:
494  cssWeight = 700;
495  break;
496  case QFont::Black:
497  cssWeight = 900;
498  break;
499  case QFont::Thin:
500  cssWeight = 100;
501  break;
502  case QFont::ExtraLight:
503  cssWeight = 200;
504  break;
505  case QFont::Medium:
506  cssWeight = 500;
507  break;
508  case QFont::ExtraBold:
509  cssWeight = 800;
510  break;
511  }
512  css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
513 
514  //size
515  css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
516 
517  return css;
518 }
519 
520 void QgsFontUtils::addRecentFontFamily( const QString &family )
521 {
522  if ( family.isEmpty() )
523  {
524  return;
525  }
526 
527  QgsSettings settings;
528  QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
529 
530  //remove matching families
531  recentFamilies.removeAll( family );
532 
533  //then add to start of list
534  recentFamilies.prepend( family );
535 
536  //trim to 10 fonts
537  recentFamilies = recentFamilies.mid( 0, 10 );
538 
539  settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
540 }
541 
543 {
544  QgsSettings settings;
545  return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
546 }
static QFont fromMimeData(const QMimeData *data, bool *ok=nullptr)
Attempts to parse the provided mime data as a QFont.
static QString buildSourcePath()
Returns path to the source directory. Valid only when running from build directory.
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:315
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 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.