QGIS API Documentation  2.18.21-Las Palmas (9fba24a)
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 
21 #include <QApplication>
22 #include <QFile>
23 #include <QFont>
24 #include <QFontDatabase>
25 #include <QFontInfo>
26 #include <QStringList>
27 
28 
30 {
31  QFontInfo fi = QFontInfo( f );
32  return fi.exactMatch();
33 }
34 
36 {
37  QFont tmpFont = QFont( family );
38  // compare just beginning of family string in case 'family [foundry]' differs
39  return tmpFont.family().startsWith( family, Qt::CaseInsensitive );
40 }
41 
42 bool QgsFontUtils::fontFamilyHasStyle( const QString& family, const QString& style )
43 {
44  QFontDatabase fontDB;
45  if ( !fontFamilyOnSystem( family ) )
46  return false;
47 
48  if ( fontDB.styles( family ).contains( style ) )
49  return true;
50 
51 #ifdef Q_OS_WIN
52  QString modified( style );
53  if ( style == "Roman" )
54  modified = "Normal";
55  if ( style == "Oblique" )
56  modified = "Italic";
57  if ( style == "Bold Oblique" )
58  modified = "Bold Italic";
59  if ( fontDB.styles( family ).contains( modified ) )
60  return true;
61 #endif
62 
63  return false;
64 }
65 
66 bool QgsFontUtils::fontFamilyMatchOnSystem( const QString& family, QString* chosen, bool* match )
67 {
68  QFontDatabase fontDB;
69  QStringList fontFamilies = fontDB.families();
70  bool found = false;
71 
72  QList<QString>::const_iterator it = fontFamilies.constBegin();
73  for ( ; it != fontFamilies.constEnd(); ++it )
74  {
75  // first compare just beginning of 'family [foundry]' string
76  if ( it->startsWith( family, Qt::CaseInsensitive ) )
77  {
78  found = true;
79  // keep looking if match info is requested
80  if ( match )
81  {
82  // full 'family [foundry]' strings have to match
83  *match = ( *it == family );
84  if ( *match )
85  break;
86  }
87  else
88  {
89  break;
90  }
91  }
92  }
93 
94  if ( found )
95  {
96  if ( chosen )
97  {
98  // retrieve the family actually assigned by matching algorithm
99  QFont f = QFont( family );
100  *chosen = f.family();
101  }
102  }
103  else
104  {
105  if ( chosen )
106  {
107  *chosen = QString();
108  }
109 
110  if ( match )
111  {
112  *match = false;
113  }
114  }
115 
116  return found;
117 }
118 
119 bool QgsFontUtils::updateFontViaStyle( QFont& f, const QString& fontstyle, bool fallback )
120 {
121  if ( fontstyle.isEmpty() )
122  {
123  return false;
124  }
125 
126  QFontDatabase fontDB;
127 
128  if ( !fallback )
129  {
130  // does the font even have the requested style?
131  bool hasstyle = fontFamilyHasStyle( f.family(), fontstyle );
132  if ( !hasstyle )
133  {
134  return false;
135  }
136  }
137 
138  // is the font's style already the same as requested?
139  if ( fontstyle == fontDB.styleString( f ) )
140  {
141  return false;
142  }
143 
144  QFont appfont = QApplication::font();
145  int defaultSize = appfont.pointSize(); // QFontDatabase::font() needs an integer for size
146 
147  QFont styledfont;
148  bool foundmatch = false;
149 
150  // if fontDB.font() fails, it returns the default app font; but, that may be the target style
151  styledfont = fontDB.font( f.family(), fontstyle, defaultSize );
152  if ( appfont != styledfont || fontstyle != fontDB.styleString( f ) )
153  {
154  foundmatch = true;
155  }
156 
157  // default to first found style if requested style is unavailable
158  // this helps in the situations where the passed-in font has to have a named style applied
159  if ( fallback && !foundmatch )
160  {
161  QFont testFont = QFont( f );
162  testFont.setPointSize( defaultSize );
163 
164  // prefer a style that mostly matches the passed-in font
165  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
166  {
167  styledfont = fontDB.font( f.family(), style, defaultSize );
168  styledfont = styledfont.resolve( f );
169  if ( testFont.toString() == styledfont.toString() )
170  {
171  foundmatch = true;
172  break;
173  }
174  }
175 
176  // fallback to first style found that works
177  if ( !foundmatch )
178  {
179  Q_FOREACH ( const QString &style, fontDB.styles( f.family() ) )
180  {
181  styledfont = fontDB.font( f.family(), style, defaultSize );
182  if ( QApplication::font() != styledfont )
183  {
184  foundmatch = true;
185  break;
186  }
187  }
188  }
189  }
190 
191  // similar to QFont::resolve, but font may already have pixel size set
192  // and we want to make sure that's preserved
193  if ( foundmatch )
194  {
195  if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
196  {
197  styledfont.setPointSizeF( f.pointSizeF() );
198  }
199  else if ( f.pixelSize() != -1 )
200  {
201  styledfont.setPixelSize( f.pixelSize() );
202  }
203  styledfont.setCapitalization( f.capitalization() );
204  styledfont.setUnderline( f.underline() );
205  styledfont.setStrikeOut( f.strikeOut() );
206  styledfont.setWordSpacing( f.wordSpacing() );
207  styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
208  f = styledfont;
209 
210  return true;
211  }
212 
213  return false;
214 }
215 
217 {
218  return "QGIS Vera Sans";
219 }
220 
222 {
223  // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
224  bool fontsLoaded = false;
225 
226  QString fontFamily = standardTestFontFamily();
227  QMap<QString, QString> fontStyles;
228  fontStyles.insert( "Roman", "QGIS-Vera/QGIS-Vera.ttf" );
229  fontStyles.insert( "Oblique", "QGIS-Vera/QGIS-VeraIt.ttf" );
230  fontStyles.insert( "Bold", "QGIS-Vera/QGIS-VeraBd.ttf" );
231  fontStyles.insert( "Bold Oblique", "QGIS-Vera/QGIS-VeraBI.ttf" );
232 
234  for ( ; f != fontStyles.constEnd(); ++f )
235  {
236  QString fontstyle( f.key() );
237  QString fontpath( f.value() );
238  if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( "All" ) ) )
239  {
240  continue;
241  }
242 
243  if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
244  {
245  fontsLoaded = ( fontsLoaded || false );
246  QgsDebugMsg( QString( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ) );
247  }
248  else
249  {
250  bool loaded = false;
252  {
253  // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
254  // from qrc resources load but fail to work and default font is substituted [LS]:
255  // https://bugreports.qt-project.org/browse/QTBUG-30917
256  // https://bugreports.qt-project.org/browse/QTBUG-32789
257  QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
258  int fontID = QFontDatabase::addApplicationFont( fontPath );
259  loaded = ( fontID != -1 );
260  fontsLoaded = ( fontsLoaded || loaded );
261  QgsDebugMsg( QString( "Test font '%1 %2' %3 from filesystem [%4]" )
262  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ) );
263  QFontDatabase db;
264  QgsDebugMsg( QString( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ) );
265  }
266  else
267  {
268  QFile fontResource( ":/testdata/font/" + fontpath );
269  if ( fontResource.open( QIODevice::ReadOnly ) )
270  {
271  int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
272  loaded = ( fontID != -1 );
273  fontsLoaded = ( fontsLoaded || loaded );
274  }
275  QgsDebugMsg( QString( "Test font '%1' (%2) %3 from testdata.qrc" )
276  .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ) );
277  }
278  }
279  }
280 
281  return fontsLoaded;
282 }
283 
284 QFont QgsFontUtils::getStandardTestFont( const QString& style, int pointsize )
285 {
286  if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
287  {
288  loadStandardTestFonts( QStringList() << style );
289  }
290 
291  QFontDatabase fontDB;
292  QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
293 #ifdef Q_OS_WIN
294  if ( !f.exactMatch() )
295  {
296  QString modified;
297  if ( style == "Roman" )
298  modified = "Normal";
299  else if ( style == "Oblique" )
300  modified = "Italic";
301  else if ( style == "Bold Oblique" )
302  modified = "Bold Italic";
303  if ( !modified.isEmpty() )
304  f = fontDB.font( standardTestFontFamily(), modified, pointsize );
305  }
306  if ( !f.exactMatch() )
307  {
308  QgsDebugMsg( QString( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
309  QgsDebugMsg( QString( "Requested: %1" ).arg( f.toString() ) );
310  QFontInfo fi( f );
311  QgsDebugMsg( QString( "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() ) );
312  }
313 #endif
314  // in case above statement fails to set style
315  f.setBold( style.contains( "Bold" ) );
316  f.setItalic( style.contains( "Oblique" ) || style.contains( "Italic" ) );
317 
318  return f;
319 }
320 
321 QDomElement QgsFontUtils::toXmlElement( const QFont& font, QDomDocument& document, const QString& elementName )
322 {
323  QDomElement fontElem = document.createElement( elementName );
324  fontElem.setAttribute( "description", font.toString() );
325  fontElem.setAttribute( "style", untranslateNamedStyle( font.styleName() ) );
326  return fontElem;
327 }
328 
329 bool QgsFontUtils::setFromXmlElement( QFont& font, const QDomElement& element )
330 {
331  if ( element.isNull() )
332  {
333  return false;
334  }
335 
336  font.fromString( element.attribute( "description" ) );
337  if ( element.hasAttribute( "style" ) )
338  {
339  ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( "style" ) ) );
340  }
341 
342  return true;
343 }
344 
345 bool QgsFontUtils::setFromXmlChildNode( QFont& font, const QDomElement& element, const QString& childNode )
346 {
347  if ( element.isNull() )
348  {
349  return false;
350  }
351 
352  QDomNodeList nodeList = element.elementsByTagName( childNode );
353  if ( !nodeList.isEmpty() )
354  {
355  QDomElement fontElem = nodeList.at( 0 ).toElement();
356  return setFromXmlElement( font, fontElem );
357  }
358  else
359  {
360  return false;
361  }
362 }
363 
365 {
366  QMap<QString, QString> translatedStyleMap;
367  QStringList words = QStringList() << "Normal" << "Light" << "Bold" << "Black" << "Demi" << "Italic" << "Oblique";
368  Q_FOREACH ( const QString& word, words )
369  {
370  translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
371  }
372  return translatedStyleMap;
373 }
374 
376 {
377  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
378  for ( int i = 0, n = words.length(); i < n; ++i )
379  {
380  words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toUtf8(), nullptr, QCoreApplication::UnicodeUTF8 );
381  }
382  return words.join( " " );
383 }
384 
386 {
387  static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
388  QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
389  for ( int i = 0, n = words.length(); i < n; ++i )
390  {
391  if ( translatedStyleMap.contains( words[i] ) )
392  {
393  words[i] = translatedStyleMap.value( words[i] );
394  }
395  else
396  {
397  QgsDebugMsg( QString( "Warning: style map does not contain %1" ).arg( words[i] ) );
398  }
399  }
400  return words.join( " " );
401 }
402 
403 QString QgsFontUtils::asCSS( const QFont& font, double pointToPixelScale )
404 {
405  QString css = QString( "font-family: " ) + font.family() + ';';
406 
407  //style
408  css += "font-style: ";
409  switch ( font.style() )
410  {
411  case QFont::StyleNormal:
412  css += "normal";
413  break;
414  case QFont::StyleItalic:
415  css += "italic";
416  break;
417  case QFont::StyleOblique:
418  css += "oblique";
419  break;
420  }
421  css += ';';
422 
423  //weight
424  int cssWeight = 400;
425  switch ( font.weight() )
426  {
427  case QFont::Light:
428  cssWeight = 300;
429  break;
430  case QFont::Normal:
431  cssWeight = 400;
432  break;
433  case QFont::DemiBold:
434  cssWeight = 600;
435  break;
436  case QFont::Bold:
437  cssWeight = 700;
438  break;
439  case QFont::Black:
440  cssWeight = 900;
441  break;
442 #if QT_VERSION >= 0x050500
443  case QFont::Thin:
444  cssWeight = 100;
445  break;
446  case QFont::ExtraLight:
447  cssWeight = 200;
448  break;
449  case QFont::Medium:
450  cssWeight = 500;
451  break;
452  case QFont::ExtraBold:
453  cssWeight = 800;
454  break;
455 #endif
456  }
457  css += QString( "font-weight: %1;" ).arg( cssWeight );
458 
459  //size
460  css += QString( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
461 
462  return css;
463 }
QDomNodeList elementsByTagName(const QString &tagname) const
void setPointSize(int pointSize)
int addApplicationFontFromData(const QByteArray &fontData)
bool contains(const Key &key) const
int pixelSize() const
QString attribute(const QString &name, const QString &defValue) const
int length() const
qreal pointSizeF() const
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
int weight() const
QStringList split(const QString &sep, SplitBehavior behavior, Qt::CaseSensitivity cs) const
const_iterator constBegin() const
static bool setFromXmlElement(QFont &font, const QDomElement &element)
Sets the properties of a font to match the properties stored in an XML element.
bool contains(const QString &str, Qt::CaseSensitivity cs) const
void setUnderline(bool enable)
static bool fontFamilyHasStyle(const QString &family, const QString &style)
Check whether font family on system has specific style.
Capitalization capitalization() const
QString join(const QString &separator) const
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.
bool qgsDoubleNear(double a, double b, double epsilon=4 *DBL_EPSILON)
Compare two doubles (but allow some difference)
Definition: qgis.h:353
QStringList styles(const QString &family) const
static QString asCSS(const QFont &font, double pointToPixelMultiplier=1.0)
Returns a CSS string representing the specified font as closely as possible.
QString styleName() const
QDomElement toElement() const
void setBold(bool enable)
bool isEmpty() const
void setPixelSize(int pixelSize)
int pixelSize() const
QFont font()
bool fromString(const QString &descrip)
bool hasAttribute(const QString &name) const
static bool fontFamilyOnSystem(const QString &family)
Check whether font family is on system in a quick manner, which does not compare [foundry].
int weight() const
qreal letterSpacing() const
void setAttribute(const QString &name, const QString &value)
QStringList applicationFontFamilies(int id)
int addApplicationFont(const QString &fileName)
static QFont getStandardTestFont(const QString &style="Roman", int pointsize=12)
Get standard test font with specific style.
bool isEmpty() const
const_iterator constEnd() const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const
QByteArray readAll()
QFont resolve(const QFont &other) const
static QMap< QString, QString > createTranslatedStyleMap()
bool underline() const
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()
Get standard test font family.
virtual bool open(QFlags< QIODevice::OpenModeFlag > mode)
QString styleString(const QFont &font)
QString family() const
void setWordSpacing(qreal spacing)
bool startsWith(const T &value) const
bool contains(QChar ch, Qt::CaseSensitivity cs) const
bool rawMode() const
QFont::Style style() const
bool exactMatch() const
void setItalic(bool enable)
void setPointSizeF(qreal pointSize)
bool isNull() const
const Key key(const T &value) const
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.
static bool fontFamilyMatchOnSystem(const QString &family, QString *chosen=nullptr, bool *match=nullptr)
Check whether font family is on system.
QString toString() const
QFont::StyleHint styleHint() const
QStringList families(WritingSystem writingSystem) const
QString family() const
static bool updateFontViaStyle(QFont &f, const QString &fontstyle, bool fallback=false)
Updates font with named style and retain all font properties.
QString translate(const char *context, const char *sourceText, const char *disambiguation, Encoding encoding)
void setStrikeOut(bool enable)
void setCapitalization(Capitalization caps)
qreal pointSizeF() const
iterator insert(const Key &key, const T &value)
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.
const_iterator constEnd() const
QDomElement createElement(const QString &tagName)
bool strikeOut() const
const_iterator constBegin() const
QFont font(const QString &family, const QString &style, int pointSize) const
bool exactMatch() const
void setLetterSpacing(SpacingType type, qreal spacing)
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
bool fixedPitch() const
int pointSize() const
Style style() const
QDomNode at(int index) const
qreal wordSpacing() const
const T value(const Key &key) const