Branch data Line data Source code
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 : 0 : bool QgsFontUtils::fontMatchOnSystem( const QFont &f )
33 : : {
34 : 0 : QFontInfo fi = QFontInfo( f );
35 : 0 : return fi.exactMatch();
36 : 0 : }
37 : :
38 : 0 : bool QgsFontUtils::fontFamilyOnSystem( const QString &family )
39 : : {
40 : 0 : QFont tmpFont = QFont( family );
41 : : // compare just beginning of family string in case 'family [foundry]' differs
42 : 0 : return tmpFont.family().startsWith( family, Qt::CaseInsensitive );
43 : 0 : }
44 : :
45 : 0 : bool QgsFontUtils::fontFamilyHasStyle( const QString &family, const QString &style )
46 : : {
47 : 0 : QFontDatabase fontDB;
48 : 0 : if ( !fontFamilyOnSystem( family ) )
49 : 0 : return false;
50 : :
51 : 0 : if ( fontDB.styles( family ).contains( style ) )
52 : 0 : 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 : 0 : return false;
67 : 0 : }
68 : :
69 : 0 : bool QgsFontUtils::fontFamilyMatchOnSystem( const QString &family, QString *chosen, bool *match )
70 : : {
71 : 0 : QFontDatabase fontDB;
72 : 0 : QStringList fontFamilies = fontDB.families();
73 : 0 : bool found = false;
74 : :
75 : 0 : QList<QString>::const_iterator it = fontFamilies.constBegin();
76 : 0 : for ( ; it != fontFamilies.constEnd(); ++it )
77 : : {
78 : : // first compare just beginning of 'family [foundry]' string
79 : 0 : if ( it->startsWith( family, Qt::CaseInsensitive ) )
80 : : {
81 : 0 : found = true;
82 : : // keep looking if match info is requested
83 : 0 : if ( match )
84 : : {
85 : : // full 'family [foundry]' strings have to match
86 : 0 : *match = ( *it == family );
87 : 0 : if ( *match )
88 : 0 : break;
89 : 0 : }
90 : : else
91 : : {
92 : 0 : break;
93 : : }
94 : 0 : }
95 : 0 : }
96 : :
97 : 0 : if ( found )
98 : : {
99 : 0 : if ( chosen )
100 : : {
101 : : // retrieve the family actually assigned by matching algorithm
102 : 0 : QFont f = QFont( family );
103 : 0 : *chosen = f.family();
104 : 0 : }
105 : 0 : }
106 : : else
107 : : {
108 : 0 : if ( chosen )
109 : : {
110 : 0 : *chosen = QString();
111 : 0 : }
112 : :
113 : 0 : if ( match )
114 : : {
115 : 0 : *match = false;
116 : 0 : }
117 : : }
118 : :
119 : 0 : return found;
120 : 0 : }
121 : :
122 : 0 : bool QgsFontUtils::updateFontViaStyle( QFont &f, const QString &fontstyle, bool fallback )
123 : : {
124 : 0 : if ( fontstyle.isEmpty() )
125 : : {
126 : 0 : return false;
127 : : }
128 : :
129 : 0 : QFontDatabase fontDB;
130 : :
131 : 0 : if ( !fallback )
132 : : {
133 : : // does the font even have the requested style?
134 : 0 : bool hasstyle = fontFamilyHasStyle( f.family(), fontstyle );
135 : 0 : if ( !hasstyle )
136 : : {
137 : 0 : return false;
138 : : }
139 : 0 : }
140 : :
141 : : // is the font's style already the same as requested?
142 : 0 : if ( fontstyle == fontDB.styleString( f ) )
143 : : {
144 : 0 : return false;
145 : : }
146 : :
147 : 0 : QFont appfont = QApplication::font();
148 : 0 : int defaultSize = appfont.pointSize(); // QFontDatabase::font() needs an integer for size
149 : :
150 : 0 : QFont styledfont;
151 : 0 : bool foundmatch = false;
152 : :
153 : : // if fontDB.font() fails, it returns the default app font; but, that may be the target style
154 : 0 : styledfont = fontDB.font( f.family(), fontstyle, defaultSize );
155 : 0 : if ( appfont != styledfont || fontstyle != fontDB.styleString( f ) )
156 : : {
157 : 0 : foundmatch = true;
158 : 0 : }
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 : 0 : if ( fallback && !foundmatch )
163 : : {
164 : 0 : QFont testFont = QFont( f );
165 : 0 : testFont.setPointSize( defaultSize );
166 : :
167 : : // prefer a style that mostly matches the passed-in font
168 : 0 : const auto constFamily = fontDB.styles( f.family() );
169 : 0 : for ( const QString &style : constFamily )
170 : : {
171 : 0 : styledfont = fontDB.font( f.family(), style, defaultSize );
172 : 0 : styledfont = styledfont.resolve( f );
173 : 0 : if ( testFont.toString() == styledfont.toString() )
174 : : {
175 : 0 : foundmatch = true;
176 : 0 : break;
177 : : }
178 : : }
179 : :
180 : : // fallback to first style found that works
181 : 0 : if ( !foundmatch )
182 : : {
183 : 0 : for ( const QString &style : constFamily )
184 : : {
185 : 0 : styledfont = fontDB.font( f.family(), style, defaultSize );
186 : 0 : if ( QApplication::font() != styledfont )
187 : : {
188 : 0 : foundmatch = true;
189 : 0 : break;
190 : : }
191 : : }
192 : 0 : }
193 : 0 : }
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 : 0 : if ( foundmatch )
198 : : {
199 : 0 : if ( !qgsDoubleNear( f.pointSizeF(), -1 ) )
200 : : {
201 : 0 : styledfont.setPointSizeF( f.pointSizeF() );
202 : 0 : }
203 : 0 : else if ( f.pixelSize() != -1 )
204 : : {
205 : 0 : styledfont.setPixelSize( f.pixelSize() );
206 : 0 : }
207 : 0 : styledfont.setCapitalization( f.capitalization() );
208 : 0 : styledfont.setUnderline( f.underline() );
209 : 0 : styledfont.setStrikeOut( f.strikeOut() );
210 : 0 : styledfont.setWordSpacing( f.wordSpacing() );
211 : 0 : styledfont.setLetterSpacing( QFont::AbsoluteSpacing, f.letterSpacing() );
212 : 0 : f = styledfont;
213 : :
214 : 0 : return true;
215 : : }
216 : :
217 : 0 : return false;
218 : 0 : }
219 : :
220 : 0 : QString QgsFontUtils::standardTestFontFamily()
221 : : {
222 : 0 : return QStringLiteral( "QGIS Vera Sans" );
223 : : }
224 : :
225 : 0 : bool QgsFontUtils::loadStandardTestFonts( const QStringList &loadstyles )
226 : : {
227 : : // load standard test font from filesystem or testdata.qrc (for unit tests and general testing)
228 : 0 : bool fontsLoaded = false;
229 : :
230 : 0 : QString fontFamily = standardTestFontFamily();
231 : 0 : QMap<QString, QString> fontStyles;
232 : 0 : fontStyles.insert( QStringLiteral( "Roman" ), QStringLiteral( "QGIS-Vera/QGIS-Vera.ttf" ) );
233 : 0 : fontStyles.insert( QStringLiteral( "Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraIt.ttf" ) );
234 : 0 : fontStyles.insert( QStringLiteral( "Bold" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBd.ttf" ) );
235 : 0 : fontStyles.insert( QStringLiteral( "Bold Oblique" ), QStringLiteral( "QGIS-Vera/QGIS-VeraBI.ttf" ) );
236 : :
237 : 0 : QMap<QString, QString>::const_iterator f = fontStyles.constBegin();
238 : 0 : for ( ; f != fontStyles.constEnd(); ++f )
239 : : {
240 : 0 : QString fontstyle( f.key() );
241 : 0 : QString fontpath( f.value() );
242 : 0 : if ( !( loadstyles.contains( fontstyle ) || loadstyles.contains( QStringLiteral( "All" ) ) ) )
243 : : {
244 : 0 : continue;
245 : : }
246 : :
247 : 0 : if ( fontFamilyHasStyle( fontFamily, fontstyle ) )
248 : : {
249 : 0 : QgsDebugMsgLevel( QStringLiteral( "Test font '%1 %2' already available" ).arg( fontFamily, fontstyle ), 2 );
250 : 0 : }
251 : : else
252 : : {
253 : 0 : bool loaded = false;
254 : 0 : if ( QgsApplication::isRunningFromBuildDir() )
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 : 0 : QString fontPath( QgsApplication::buildSourcePath() + "/tests/testdata/font/" + fontpath );
261 : 0 : int fontID = QFontDatabase::addApplicationFont( fontPath );
262 : 0 : loaded = ( fontID != -1 );
263 : 0 : fontsLoaded = ( fontsLoaded || loaded );
264 : 0 : QgsDebugMsgLevel( QStringLiteral( "Test font '%1 %2' %3 from filesystem [%4]" )
265 : : .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load", fontPath ), 2 );
266 : 0 : QFontDatabase db;
267 : 0 : QgsDebugMsgLevel( QStringLiteral( "font families in %1: %2" ).arg( fontID ).arg( db.applicationFontFamilies( fontID ).join( "," ) ), 2 );
268 : 0 : }
269 : : else
270 : : {
271 : 0 : QFile fontResource( ":/testdata/font/" + fontpath );
272 : 0 : if ( fontResource.open( QIODevice::ReadOnly ) )
273 : : {
274 : 0 : int fontID = QFontDatabase::addApplicationFontFromData( fontResource.readAll() );
275 : 0 : loaded = ( fontID != -1 );
276 : 0 : fontsLoaded = ( fontsLoaded || loaded );
277 : 0 : }
278 : 0 : QgsDebugMsgLevel( QStringLiteral( "Test font '%1' (%2) %3 from testdata.qrc" )
279 : : .arg( fontFamily, fontstyle, loaded ? "loaded" : "FAILED to load" ), 2 );
280 : 0 : }
281 : : }
282 : 0 : }
283 : :
284 : 0 : return fontsLoaded;
285 : 0 : }
286 : :
287 : 0 : QFont QgsFontUtils::getStandardTestFont( const QString &style, int pointsize )
288 : : {
289 : 0 : if ( ! fontFamilyHasStyle( standardTestFontFamily(), style ) )
290 : : {
291 : 0 : loadStandardTestFonts( QStringList() << style );
292 : 0 : }
293 : :
294 : 0 : QFontDatabase fontDB;
295 : 0 : 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 : 0 : f.setBold( style.contains( QLatin1String( "Bold" ) ) );
319 : 0 : f.setItalic( style.contains( QLatin1String( "Oblique" ) ) || style.contains( QLatin1String( "Italic" ) ) );
320 : :
321 : 0 : return f;
322 : 0 : }
323 : :
324 : 0 : QDomElement QgsFontUtils::toXmlElement( const QFont &font, QDomDocument &document, const QString &elementName )
325 : : {
326 : 0 : QDomElement fontElem = document.createElement( elementName );
327 : 0 : fontElem.setAttribute( QStringLiteral( "description" ), font.toString() );
328 : 0 : fontElem.setAttribute( QStringLiteral( "style" ), untranslateNamedStyle( font.styleName() ) );
329 : 0 : return fontElem;
330 : 0 : }
331 : :
332 : 0 : bool QgsFontUtils::setFromXmlElement( QFont &font, const QDomElement &element )
333 : : {
334 : 0 : if ( element.isNull() )
335 : : {
336 : 0 : return false;
337 : : }
338 : :
339 : 0 : font.fromString( element.attribute( QStringLiteral( "description" ) ) );
340 : 0 : if ( element.hasAttribute( QStringLiteral( "style" ) ) )
341 : : {
342 : 0 : ( void )updateFontViaStyle( font, translateNamedStyle( element.attribute( QStringLiteral( "style" ) ) ) );
343 : 0 : }
344 : :
345 : 0 : return true;
346 : 0 : }
347 : :
348 : 0 : bool QgsFontUtils::setFromXmlChildNode( QFont &font, const QDomElement &element, const QString &childNode )
349 : : {
350 : 0 : if ( element.isNull() )
351 : : {
352 : 0 : return false;
353 : : }
354 : :
355 : 0 : QDomNodeList nodeList = element.elementsByTagName( childNode );
356 : 0 : if ( !nodeList.isEmpty() )
357 : : {
358 : 0 : QDomElement fontElem = nodeList.at( 0 ).toElement();
359 : 0 : return setFromXmlElement( font, fontElem );
360 : 0 : }
361 : : else
362 : : {
363 : 0 : return false;
364 : : }
365 : 0 : }
366 : :
367 : 0 : QMimeData *QgsFontUtils::toMimeData( const QFont &font )
368 : : {
369 : 0 : std::unique_ptr< QMimeData >mimeData( new QMimeData );
370 : :
371 : 0 : QDomDocument fontDoc;
372 : 0 : QDomElement fontElem = toXmlElement( font, fontDoc, QStringLiteral( "font" ) );
373 : 0 : fontDoc.appendChild( fontElem );
374 : 0 : mimeData->setText( fontDoc.toString() );
375 : :
376 : 0 : return mimeData.release();
377 : 0 : }
378 : :
379 : 0 : QFont QgsFontUtils::fromMimeData( const QMimeData *data, bool *ok )
380 : : {
381 : 0 : QFont font;
382 : 0 : if ( ok )
383 : 0 : *ok = false;
384 : :
385 : 0 : if ( !data )
386 : 0 : return font;
387 : :
388 : 0 : QString text = data->text();
389 : 0 : if ( !text.isEmpty() )
390 : : {
391 : 0 : QDomDocument doc;
392 : 0 : QDomElement elem;
393 : :
394 : 0 : if ( doc.setContent( text ) )
395 : : {
396 : 0 : elem = doc.documentElement();
397 : :
398 : 0 : if ( elem.nodeName() != QLatin1String( "font" ) )
399 : 0 : elem = elem.firstChildElement( QStringLiteral( "font" ) );
400 : :
401 : 0 : if ( setFromXmlElement( font, elem ) )
402 : : {
403 : 0 : if ( ok )
404 : 0 : *ok = true;
405 : 0 : }
406 : 0 : return font;
407 : : }
408 : 0 : }
409 : 0 : return font;
410 : 0 : }
411 : :
412 : 0 : static QMap<QString, QString> createTranslatedStyleMap()
413 : : {
414 : 0 : QMap<QString, QString> translatedStyleMap;
415 : 0 : QStringList words = QStringList()
416 : 0 : << QStringLiteral( "Normal" )
417 : 0 : << QStringLiteral( "Regular" )
418 : 0 : << QStringLiteral( "Light" )
419 : 0 : << QStringLiteral( "Bold" )
420 : 0 : << QStringLiteral( "Black" )
421 : 0 : << QStringLiteral( "Demi" )
422 : 0 : << QStringLiteral( "Italic" )
423 : 0 : << QStringLiteral( "Oblique" );
424 : 0 : const auto constWords = words;
425 : 0 : for ( const QString &word : constWords )
426 : : {
427 : 0 : translatedStyleMap.insert( QCoreApplication::translate( "QFontDatabase", qPrintable( word ) ), word );
428 : : }
429 : 0 : return translatedStyleMap;
430 : 0 : }
431 : :
432 : 0 : QString QgsFontUtils::translateNamedStyle( const QString &namedStyle )
433 : : {
434 : : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
435 : : QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
436 : : #else
437 : 0 : QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
438 : : #endif
439 : 0 : for ( int i = 0, n = words.length(); i < n; ++i )
440 : : {
441 : 0 : words[i] = QCoreApplication::translate( "QFontDatabase", words[i].toLocal8Bit().constData() );
442 : 0 : }
443 : 0 : return words.join( QLatin1Char( ' ' ) );
444 : 0 : }
445 : :
446 : 0 : QString QgsFontUtils::untranslateNamedStyle( const QString &namedStyle )
447 : : {
448 : 0 : static QMap<QString, QString> translatedStyleMap = createTranslatedStyleMap();
449 : : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
450 : : QStringList words = namedStyle.split( ' ', QString::SkipEmptyParts );
451 : : #else
452 : 0 : QStringList words = namedStyle.split( ' ', Qt::SkipEmptyParts );
453 : : #endif
454 : :
455 : 0 : for ( int i = 0, n = words.length(); i < n; ++i )
456 : : {
457 : 0 : if ( translatedStyleMap.contains( words[i] ) )
458 : : {
459 : 0 : words[i] = translatedStyleMap.value( words[i] );
460 : 0 : }
461 : : else
462 : : {
463 : 0 : QgsDebugMsgLevel( QStringLiteral( "Warning: style map does not contain %1" ).arg( words[i] ), 2 );
464 : : }
465 : 0 : }
466 : 0 : return words.join( QLatin1Char( ' ' ) );
467 : 0 : }
468 : :
469 : 0 : QString QgsFontUtils::asCSS( const QFont &font, double pointToPixelScale )
470 : : {
471 : 0 : QString css = QStringLiteral( "font-family: " ) + font.family() + ';';
472 : :
473 : : //style
474 : 0 : css += QLatin1String( "font-style: " );
475 : 0 : switch ( font.style() )
476 : : {
477 : : case QFont::StyleNormal:
478 : 0 : css += QLatin1String( "normal" );
479 : 0 : break;
480 : : case QFont::StyleItalic:
481 : 0 : css += QLatin1String( "italic" );
482 : 0 : break;
483 : : case QFont::StyleOblique:
484 : 0 : css += QLatin1String( "oblique" );
485 : 0 : break;
486 : : }
487 : 0 : css += ';';
488 : :
489 : : //weight
490 : 0 : int cssWeight = 400;
491 : 0 : switch ( font.weight() )
492 : : {
493 : : case QFont::Light:
494 : 0 : cssWeight = 300;
495 : 0 : break;
496 : : case QFont::Normal:
497 : 0 : cssWeight = 400;
498 : 0 : break;
499 : : case QFont::DemiBold:
500 : 0 : cssWeight = 600;
501 : 0 : break;
502 : : case QFont::Bold:
503 : 0 : cssWeight = 700;
504 : 0 : break;
505 : : case QFont::Black:
506 : 0 : cssWeight = 900;
507 : 0 : break;
508 : : case QFont::Thin:
509 : 0 : cssWeight = 100;
510 : 0 : break;
511 : : case QFont::ExtraLight:
512 : 0 : cssWeight = 200;
513 : 0 : break;
514 : : case QFont::Medium:
515 : 0 : cssWeight = 500;
516 : 0 : break;
517 : : case QFont::ExtraBold:
518 : 0 : cssWeight = 800;
519 : 0 : break;
520 : : }
521 : 0 : css += QStringLiteral( "font-weight: %1;" ).arg( cssWeight );
522 : :
523 : : //size
524 : 0 : css += QStringLiteral( "font-size: %1px;" ).arg( font.pointSizeF() >= 0 ? font.pointSizeF() * pointToPixelScale : font.pixelSize() );
525 : :
526 : 0 : return css;
527 : 0 : }
528 : :
529 : 0 : void QgsFontUtils::addRecentFontFamily( const QString &family )
530 : : {
531 : 0 : if ( family.isEmpty() )
532 : : {
533 : 0 : return;
534 : : }
535 : :
536 : 0 : QgsSettings settings;
537 : 0 : QStringList recentFamilies = settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
538 : :
539 : : //remove matching families
540 : 0 : recentFamilies.removeAll( family );
541 : :
542 : : //then add to start of list
543 : 0 : recentFamilies.prepend( family );
544 : :
545 : : //trim to 10 fonts
546 : 0 : recentFamilies = recentFamilies.mid( 0, 10 );
547 : :
548 : 0 : settings.setValue( QStringLiteral( "fonts/recent" ), recentFamilies );
549 : 0 : }
550 : :
551 : 0 : QStringList QgsFontUtils::recentFontFamilies()
552 : : {
553 : 0 : QgsSettings settings;
554 : 0 : return settings.value( QStringLiteral( "fonts/recent" ) ).toStringList();
555 : 0 : }
|