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