QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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 QgsDebugMsg( QStringLiteral( "Inexact font match - consider installing the %1 font." ).arg( standardTestFontFamily() ) );
342 QgsDebugMsg( QStringLiteral( "Requested: %1" ).arg( f.toString() ) );
343 QFontInfo fi( f );
344 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() ) );
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#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
487 QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
488#else
489 QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
490#endif
491 for ( int i = 0, n = words.length(); i < n; ++i )
492 {
493 words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toLocal8Bit().constData() );
494 }
495 return words.join( QLatin1Char( ' ' ) );
496}
497
498QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
499{
500 static const QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
501#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
502 QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
503#else
504 QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
505#endif
506
507 for ( int i = 0, n = words.length(); i < n; ++i )
508 {
509 if ( translatedStyleMap.contains( words[i] ) )
510 {
511 words[i] = translatedStyleMap.value( words[i] );
512 }
513 else
514 {
515 QgsDebugMsgLevel( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ), 2 );
516 }
517 }
518 return words.join( QLatin1Char( ' ' ) );
519}
520
521QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
522{
523 QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
524
525 //style
526 css += QLatin1String( "font-style: " );
527 switch ( font.style() )
528 {
529 case QFont::StyleNormal:
530 css += QLatin1String( "normal" );
531 break;
532 case QFont::StyleItalic:
533 css += QLatin1String( "italic" );
534 break;
535 case QFont::StyleOblique:
536 css += QLatin1String( "oblique" );
537 break;
538 }
539 css += ';';
540
541 //weight
542 int cssWeight = 400;
543 switch ( font.weight() )
544 {
545 case QFont::Light:
546 cssWeight = 300;
547 break;
548 case QFont::Normal:
549 cssWeight = 400;
550 break;
551 case QFont::DemiBold:
552 cssWeight = 600;
553 break;
554 case QFont::Bold:
555 cssWeight = 700;
556 break;
557 case QFont::Black:
558 cssWeight = 900;
559 break;
560 case QFont::Thin:
561 cssWeight = 100;
562 break;
563 case QFont::ExtraLight:
564 cssWeight = 200;
565 break;
566 case QFont::Medium:
567 cssWeight = 500;
568 break;
569 case QFont::ExtraBold:
570 cssWeight = 800;
571 break;
572 }
573 css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
574
575 //size
576 css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
577
578 return css;
579}
580
581void QgsFontUtils::addRecentFontFamily( const QString &family )
582{
583 if ( family.isEmpty() )
584 {
585 return;
586 }
587
588 QgsSettings settings;
589 QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
590
591 //remove matching families
592 recentFamilies.removeAll( family );
593
594 //then add to start of list
595 recentFamilies.prepend( family );
596
597 //trim to 10 fonts
598 recentFamilies = recentFamilies.mid( 0, 10 );
599
600 settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
601}
602
604{
605 const QgsSettings settings;
606 return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
607}
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 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.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
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:2527
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38