QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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
32bool QgsFontUtils::fontMatchOnSystem( const QFont &f )
33{
34 const QFontInfo fi = QFontInfo( f );
35 return fi.exactMatch();
36}
37
38bool 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
45bool 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
69QString QgsFontUtils::resolveFontStyleName( const QFont &font )
70{
71 auto styleNameIsMatch = [&font]( const QString & candidate ) -> bool
72 {
73 // confirm that style name matches bold/italic flags
74 QFont testFont( font.family() );
75 testFont.setStyleName( candidate );
76 return testFont.italic() == font.italic() && testFont.weight() == font.weight();
77 };
78
79 // attempt 1
80 const QFontInfo fontInfo( font );
81 QString styleName = fontInfo.styleName();
82 if ( !styleName.isEmpty() )
83 {
84 if ( styleNameIsMatch( styleName ) )
85 return styleName;
86 }
87
88 // attempt 2
89 styleName = QFontDatabase().styleString( font );
90 if ( !styleName.isEmpty() )
91 {
92 if ( styleNameIsMatch( styleName ) )
93 return styleName;
94 }
95
96 // failed
97 return QString();
98}
99
100bool QgsFontUtils::fontFamilyMatchOnSystem( const QString &family, QString *chosen, bool *match )
101{
102 const QFontDatabase fontDB;
103 const QStringList fontFamilies = fontDB.families();
104 bool found = false;
105
106 QList<QString>::const_iterator it = fontFamilies.constBegin();
107 for ( ; it != fontFamilies.constEnd(); ++it )
108 {
109 // first compare just beginning of 'family [foundry]' string
110 if ( it->startsWith( family, Qt::CaseInsensitive ) )
111 {
112 found = true;
113 // keep looking if match info is requested
114 if ( match )
115 {
116 // full 'family [foundry]' strings have to match
117 *match = ( *it == family );
118 if ( *match )
119 break;
120 }
121 else
122 {
123 break;
124 }
125 }
126 }
127
128 if ( found )
129 {
130 if ( chosen )
131 {
132 // retrieve the family actually assigned by matching algorithm
133 const QFont f = QFont( family );
134 *chosen = f.family();
135 }
136 }
137 else
138 {
139 if ( chosen )
140 {
141 *chosen = QString();
142 }
143
144 if ( match )
145 {
146 *match = false;
147 }
148 }
149
150 return found;
151}
152
153bool QgsFontUtils::updateFontViaStyle( QFont &f, const QString &fontstyle, bool fallback )
154{
155 if ( fontstyle.isEmpty() )
156 {
157 return false;
158 }
159
160 QFontDatabase fontDB;
161
162 if ( !fallback )
163 {
164 // does the font even have the requested style?
165 const bool hasstyle = fontFamilyHasStyle( f.family(), fontstyle );
166 if ( !hasstyle )
167 {
168 return false;
169 }
170 }
171
172 // is the font's style already the same as requested?
173 if ( fontstyle == fontDB.styleString( f ) )
174 {
175 return false;
176 }
177
178 const QFont appfont = QApplication::font();
179 const int defaultSize = appfont.pointSize(); // QFontDatabase::font() needs an integer for size
180
181 QFont styledfont;
182 bool foundmatch = false;
183
184 // if fontDB.font() fails, it returns the default app font; but, that may be the target style
185 styledfont = fontDB.font( f.family(), fontstyle, defaultSize );
186 if ( appfont != styledfont || fontstyle != fontDB.styleString( f ) )
187 {
188 foundmatch = true;
189 }
190
191 // default to first found style if requested style is unavailable
192 // this helps in the situations where the passed-in font has to have a named style applied
193 if ( fallback && !foundmatch )
194 {
195 QFont testFont = QFont( f );
196 testFont.setPointSize( defaultSize );
197
198 // prefer a style that mostly matches the passed-in font
199 const auto constFamily = fontDB.styles( f.family() );
200 for ( const QString &style : constFamily )
201 {
202 styledfont = fontDB.font( f.family(), style, defaultSize );
203 styledfont = styledfont.resolve( f );
204 if ( testFont.toString() == styledfont.toString() )
205 {
206 foundmatch = true;
207 break;
208 }
209 }
210
211 // fallback to first style found that works
212 if ( !foundmatch )
213 {
214 for ( const QString &style : constFamily )
215 {
216 styledfont = fontDB.font( f.family(), style, defaultSize );
217 if ( QApplication::font() != styledfont )
218 {
219 foundmatch = true;
220 break;
221 }
222 }
223 }
224 }
225
226 // similar to QFont::resolve, but font may already have pixel size set
227 // and we want to make sure that's preserved
228 if ( foundmatch )
229 {
230 if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
231 {
232 styledfont.setPointSizeF( f.pointSizeF() );
233 }
234 else if ( f.pixelSize() != -1 )
235 {
236 styledfont.setPixelSize( f.pixelSize() );
237 }
238 styledfont.setCapitalization( f.capitalization() );
239 styledfont.setUnderline( f.underline() );
240 styledfont.setStrikeOut( f.strikeOut() );
241 styledfont.setWordSpacing( f.wordSpacing() );
242 styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
243 f = styledfont;
244
245 return true;
246 }
247
248 return false;
249}
250
252{
253 return QStringLiteral( "QGIS Vera Sans" );
254}
255
256bool QgsFontUtils::loadStandardTestFonts( const QStringList &loadstyles )
257{
258 // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
259 bool fontsLoaded = false;
260
261 const QString fontFamily = standardTestFontFamily();
262 QMap<QString, QString> fontStyles;
263 fontStyles.insert( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
264 fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
265 fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
266 fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
267
268 QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
269 for ( ; f != fontStyles.constEnd(); ++f )
270 {
271 const QString fontstyle( f.key() );
272 const QString fontpath( f.value() );
273 if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
274 {
275 continue;
276 }
277
278 if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
279 {
280 QgsDebugMsgLevel( QStringLiteral( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ), 2 );
281 }
282 else
283 {
284 bool loaded = false;
286 {
287 // workaround for bugs with Qt 4.8.5 (other versions?) on Mac 10.9, where fonts
288 // from qrc resources load but fail to work and default font is substituted [LS]:
289 // https://bugreports.qt.io/browse/QTBUG-30917
290 // https://bugreports.qt.io/browse/QTBUG-32789
291 const QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
292 const int fontID = QFontDatabase::addApplicationFont( fontPath );
293 loaded = ( fontID != -1 );
294 fontsLoaded = ( fontsLoaded || loaded );
295 QgsDebugMsgLevel( QStringLiteral( "Test font '%1 %2' %3 from filesystem [%4]" )
296 .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ), 2 );
297 QgsDebugMsgLevel( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( QFontDatabase().applicationFontFamilies( fontID ).join( "," ) ), 2 );
298 }
299 else
300 {
301 QFile fontResource( ":/testdata/font/" + fontpath );
302 if ( fontResource.open( QIODevice::ReadOnly ) )
303 {
304 const int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
305 loaded = ( fontID != -1 );
306 fontsLoaded = ( fontsLoaded || loaded );
307 }
308 QgsDebugMsgLevel( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
309 .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ), 2 );
310 }
311 }
312 }
313
314 return fontsLoaded;
315}
316
317QFont QgsFontUtils::getStandardTestFont( const QString &style, int pointsize )
318{
319 if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
320 {
321 loadStandardTestFonts( QStringList() << style );
322 }
323
324 const QFontDatabase fontDB;
325 QFont f = fontDB.font( standardTestFontFamily(), style, pointsize );
326#ifdef Q_OS_WIN
327 if ( !f.exactMatch() )
328 {
329 QString modified;
330 if ( style == "Roman" )
331 modified = "Normal";
332 else if ( style == "Oblique" )
333 modified = "Italic";
334 else if ( style == "Bold Oblique" )
335 modified = "Bold Italic";
336 if ( !modified.isEmpty() )
337 f = fontDB.font( standardTestFontFamily(), modified, pointsize );
338 }
339 if ( !f.exactMatch() )
340 {
341 QgsDebugMsgLevel( QStringLiteral( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ), 2 );
342 QgsDebugMsgLevel( QStringLiteral( "Requested: %1" ).arg( f.toString() ), 2 );
343 QFontInfo fi( f );
344 QgsDebugMsgLevel( 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() ), 2 );
345 }
346#endif
347 // in case above statement fails to set style
348 f.setBold( style.contains( QLatin1String( "Bold" ) ) );
349 f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "Italic" ) ) );
350
351 return f;
352}
353
354QDomElement QgsFontUtils::toXmlElement( const QFont &font, QDomDocument &document, const QString &elementName )
355{
356 QDomElement fontElem = document.createElement( elementName );
357 fontElem.setAttribute( QStringLiteral( "description" ), font.toString() );
358 fontElem.setAttribute( QStringLiteral( "style" ), untranslateNamedStyle( font.styleName() ) );
359 fontElem.setAttribute( QStringLiteral( "bold" ), font.bold() ? QChar( '1' ) : QChar( '0' ) );
360 fontElem.setAttribute( QStringLiteral( "italic" ), font.italic() ? QChar( '1' ) : QChar( '0' ) );
361 fontElem.setAttribute( QStringLiteral( "underline" ), font.underline() ? QChar( '1' ) : QChar( '0' ) );
362 fontElem.setAttribute( QStringLiteral( "strikethrough" ), font.strikeOut() ? QChar( '1' ) : QChar( '0' ) );
363 return fontElem;
364}
365
366bool QgsFontUtils::setFromXmlElement( QFont &font, const QDomElement &element )
367{
368 if ( element.isNull() )
369 {
370 return false;
371 }
372
373 font.fromString( element.attribute( QStringLiteral( "description" ) ) );
374
375 if ( element.hasAttribute( QStringLiteral( "bold" ) ) && element.attribute( QStringLiteral( "bold" ) ) == QChar( '1' ) )
376 {
377 font.setBold( true );
378 }
379 if ( element.hasAttribute( QStringLiteral( "italic" ) ) )
380 {
381 font.setItalic( element.attribute( QStringLiteral( "italic" ) ) == QChar( '1' ) );
382 }
383 if ( element.hasAttribute( QStringLiteral( "underline" ) ) )
384 {
385 font.setUnderline( element.attribute( QStringLiteral( "underline" ) ) == QChar( '1' ) );
386 }
387 if ( element.hasAttribute( QStringLiteral( "strikethrough" ) ) )
388 {
389 font.setStrikeOut( element.attribute( QStringLiteral( "strikethrough" ) ) == QChar( '1' ) );
390 }
391
392 if ( element.hasAttribute( QStringLiteral( "style" ) ) )
393 {
394 ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "style" ) ) ) );
395 }
396
397 return true;
398}
399
400bool QgsFontUtils::setFromXmlChildNode( QFont &font, const QDomElement &element, const QString &childNode )
401{
402 if ( element.isNull() )
403 {
404 return false;
405 }
406
407 const QDomNodeList nodeList = element.elementsByTagName( childNode );
408 if ( !nodeList.isEmpty() )
409 {
410 const QDomElement fontElem = nodeList.at( 0 ).toElement();
411 return setFromXmlElement( font, fontElem );
412 }
413 else
414 {
415 return false;
416 }
417}
418
419QMimeData *QgsFontUtils::toMimeData( const QFont &font )
420{
421 std::unique_ptr< QMimeData >mimeData( new QMimeData );
422
423 QDomDocument fontDoc;
424 const QDomElement fontElem = toXmlElement( font, fontDoc, QStringLiteral( "font" ) );
425 fontDoc.appendChild( fontElem );
426 mimeData->setText( fontDoc.toString() );
427
428 return mimeData.release();
429}
430
431QFont QgsFontUtils::fromMimeData( const QMimeData *data, bool *ok )
432{
433 QFont font;
434 if ( ok )
435 *ok = false;
436
437 if ( !data )
438 return font;
439
440 const QString text = data->text();
441 if ( !text.isEmpty() )
442 {
443 QDomDocument doc;
444 QDomElement elem;
445
446 if ( doc.setContent( text ) )
447 {
448 elem = doc.documentElement();
449
450 if ( elem.nodeName() != QLatin1String( "font" ) )
451 elem = elem.firstChildElement( QStringLiteral( "font" ) );
452
453 if ( setFromXmlElement( font, elem ) )
454 {
455 if ( ok )
456 *ok = true;
457 }
458 return font;
459 }
460 }
461 return font;
462}
463
464static QMap<QString, QString> createTranslatedStyleMap()
465{
466 QMap<QString, QString> translatedStyleMap;
467 const QStringList words = QStringList()
468 << QStringLiteral( "Normal" )
469 << QStringLiteral( "Regular" )
470 << QStringLiteral( "Light" )
471 << QStringLiteral( "Bold" )
472 << QStringLiteral( "Black" )
473 << QStringLiteral( "Demi" )
474 << QStringLiteral( "Italic" )
475 << QStringLiteral( "Oblique" );
476 const auto constWords = words;
477 for ( const QString &word : constWords )
478 {
479 translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
480 }
481 return translatedStyleMap;
482}
483
484QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
485{
486 QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
487 for ( int i = 0, n = words.length(); i < n; ++i )
488 {
489 words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toLocal8Bit().constData() );
490 }
491 return words.join( QLatin1Char( ' ' ) );
492}
493
494QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
495{
496 static const QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
497 QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
498
499 for ( int i = 0, n = words.length(); i < n; ++i )
500 {
501 if ( translatedStyleMap.contains( words[i] ) )
502 {
503 words[i] = translatedStyleMap.value( words[i] );
504 }
505 else
506 {
507 QgsDebugMsgLevel( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ), 2 );
508 }
509 }
510 return words.join( QLatin1Char( ' ' ) );
511}
512
513QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
514{
515 QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
516
517 //style
518 css += QLatin1String( "font-style: " );
519 switch ( font.style() )
520 {
521 case QFont::StyleNormal:
522 css += QLatin1String( "normal" );
523 break;
524 case QFont::StyleItalic:
525 css += QLatin1String( "italic" );
526 break;
527 case QFont::StyleOblique:
528 css += QLatin1String( "oblique" );
529 break;
530 }
531 css += ';';
532
533 //weight
534 int cssWeight = 400;
535 switch ( font.weight() )
536 {
537 case QFont::Light:
538 cssWeight = 300;
539 break;
540 case QFont::Normal:
541 cssWeight = 400;
542 break;
543 case QFont::DemiBold:
544 cssWeight = 600;
545 break;
546 case QFont::Bold:
547 cssWeight = 700;
548 break;
549 case QFont::Black:
550 cssWeight = 900;
551 break;
552 case QFont::Thin:
553 cssWeight = 100;
554 break;
555 case QFont::ExtraLight:
556 cssWeight = 200;
557 break;
558 case QFont::Medium:
559 cssWeight = 500;
560 break;
561 case QFont::ExtraBold:
562 cssWeight = 800;
563 break;
564 }
565 css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
566
567 //size
568 css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
569
570 return css;
571}
572
573void QgsFontUtils::addRecentFontFamily( const QString &family )
574{
575 if ( family.isEmpty() )
576 {
577 return;
578 }
579
580 QgsSettings settings;
581 QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
582
583 //remove matching families
584 recentFamilies.removeAll( family );
585
586 //then add to start of list
587 recentFamilies.prepend( family );
588
589 //trim to 10 fonts
590 recentFamilies = recentFamilies.mid( 0, 10 );
591
592 settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
593}
594
596{
597 const QgsSettings settings;
598 return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
599}
600
601void QgsFontUtils::setFontFamily( QFont &font, const QString &family )
602{
603 font.setFamily( family );
604 if ( !font.exactMatch() )
605 {
606 // some Qt versions struggle with fonts with certain unusual characters
607 // in their names, eg "ESRI Oil, Gas, & Water". Calling "setFamilies"
608 // can workaround these issues... (in some cases!)
609 font.setFamilies( { family } );
610 }
611}
612
613QFont QgsFontUtils::createFont( const QString &family, int pointSize, int weight, bool italic )
614{
615 QFont font( family, pointSize, weight, italic );
616 if ( !font.exactMatch() )
617 {
618 // some Qt versions struggle with fonts with certain unusual characters
619 // in their names, eg "ESRI Oil, Gas, & Water". Calling "setFamilies"
620 // can workaround these issues... (in some cases!)
621 font.setFamilies( { family } );
622 }
623 return font;
624}
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 resolveFontStyleName(const QFont &font)
Attempts to resolve the style name corresponding to the specified font object.
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 QFont createFont(const QString &family, int pointSize=-1, int weight=-1, bool italic=false)
Creates a font with the specified family.
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 void setFontFamily(QFont &font, const QString &family)
Sets the family for a font object.
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.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:64
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39