18#include <nlohmann/json.hpp>
30#include <QFontDatabase>
31#include <QRegularExpression>
32#include <QRegularExpressionMatch>
34#include <QTemporaryDir>
35#include <QTemporaryFile>
37#include "moc_qgsfontmanager.cpp"
39using namespace Qt::StringLiterals;
62 const thread_local QRegularExpression charsToRemove( u
"[^a-z]"_s );
63 const thread_local QRegularExpression styleNames( u
"(?:normal|regular|light|bold|black|demi|italic|oblique|medium|thin)"_s );
65 QString processed =
family.toLower();
66 processed.replace( styleNames, QString() );
67 return processed.replace( charsToRemove, QString() );
78 for (
const QString &replacement : replacements )
80 const thread_local QRegularExpression rxReplacement( u
"(.*?):(.*)"_s );
81 const QRegularExpressionMatch match = rxReplacement.match( replacement );
82 if ( match.hasMatch() )
84 mFamilyReplacements.insert( match.captured( 1 ), match.captured( 2 ) );
85 mLowerCaseFamilyReplacements.insert( match.captured( 1 ).toLower(), match.captured( 2 ) );
93 return mFamilyReplacements;
99 if ( !replacement.isEmpty() )
101 mFamilyReplacements.insert( original, replacement );
102 mLowerCaseFamilyReplacements.insert( original.toLower(), replacement );
106 mFamilyReplacements.remove( original );
107 mLowerCaseFamilyReplacements.remove( original.toLower() );
109 storeFamilyReplacements();
115 mFamilyReplacements = replacements;
116 mLowerCaseFamilyReplacements.clear();
117 for (
auto it = mFamilyReplacements.constBegin(); it != mFamilyReplacements.constEnd(); ++it )
118 mLowerCaseFamilyReplacements.insert( it.key().toLower(), it.value() );
120 storeFamilyReplacements();
126 auto it = mLowerCaseFamilyReplacements.constFind( name.toLower() );
127 if ( it != mLowerCaseFamilyReplacements.constEnd() )
133void QgsFontManager::storeFamilyReplacements()
135 QStringList replacements;
136 for (
auto it = mFamilyReplacements.constBegin(); it != mFamilyReplacements.constEnd(); ++it )
137 replacements << u
"%1:%2"_s.arg( it.key(), it.value() );
145 QStringList fontDirs { userProfileFontsDir };
147 fontDirs.append( mUserFontDirectories );
149 for (
const QString &dir : std::as_const( fontDirs ) )
151 if ( !QFile::exists( dir ) && !QDir().mkpath( dir ) )
153 QgsDebugError( u
"Cannot create local fonts dir: %1"_s.arg( dir ) );
157 installFontsFromDirectory( dir );
161void QgsFontManager::installFontsFromDirectory(
const QString &dir )
163 const QFileInfoList fileInfoList = QDir( dir ).entryInfoList( QStringList( u
"*"_s ), QDir::Files );
164 QFileInfoList::const_iterator infoIt = fileInfoList.constBegin();
165 for ( ; infoIt != fileInfoList.constEnd(); ++infoIt )
167 const int id = QFontDatabase::addApplicationFont( infoIt->filePath() );
170 QgsDebugError( u
"The user font %1 could not be installed"_s.arg( infoIt->filePath() ) );
171 mUserFontToFamilyMap.remove( infoIt->filePath() );
172 mUserFontToIdMap.remove( infoIt->filePath() );
176 mUserFontToFamilyMap.insert( infoIt->filePath(), QFontDatabase::applicationFontFamilies(
id ) );
177 mUserFontToIdMap.insert( infoIt->filePath(),
id );
184 matchedFamily.clear();
189 auto it = mPendingFontDownloads.constFind( family );
190 if ( it != mPendingFontDownloads.constEnd() )
192 matchedFamily = it.value();
204 const QFont testFont( matchedFamily );
205 if ( testFont.exactMatch() )
209 mPendingFontDownloads.insert( family, matchedFamily );
210 if ( !mEnableFontDownloads )
212 mDeferredFontDownloads.insert( matchedFamily, details );
224 if ( mEnableFontDownloads )
227 mEnableFontDownloads =
true;
229 if ( !mDeferredFontDownloads.isEmpty() )
232 for (
auto it = mDeferredFontDownloads.constBegin(); it != mDeferredFontDownloads.constEnd(); ++it )
236 mDeferredFontDownloads.clear();
242 QStringList fontUrls;
243 fontUrls.reserve( downloadPaths.size() );
244 for (
const QString &path : downloadPaths )
246 fontUrls.append( u
"https://github.com/google/fonts/raw/main/%1"_s.arg( path ) );
248 return QgsFontDownloadDetails( family, fontUrls, !licensePath.isEmpty() ? u
"https://github.com/google/fonts/raw/main/%1"_s.arg( licensePath ) : QString() );
253 std::vector< QgsFontDownloadDetails > fonts;
257 QFile file( jsonPath );
258 if ( !file.open( QIODevice::ReadOnly ) )
260 QgsDebugError( u
"Failed to open Google fonts JSON file: %1"_s.arg( jsonPath ) );
264 const QByteArray jsonContent = file.readAll();
267 const json fontsJson = json::parse( jsonContent.toStdString() );
268 if ( fontsJson.is_array() )
270 fonts.reserve( fontsJson.size() );
271 for (
const json &fontJson : fontsJson )
273 const QString family = QString::fromStdString( fontJson[
"family"].get<std::string>() );
274 const QString license = QString::fromStdString( fontJson[
"license"].get<std::string>() );
277 const json &pathsArray = fontJson[
"paths"];
278 if ( !pathsArray.is_array() )
280 QgsDebugError( u
"Failed to parse Google font %1, expected array for paths."_s.arg( family ) );
283 for (
const json &pathJson : pathsArray )
285 paths.append( QString::fromStdString( pathJson.get<std::string>() ) );
293 QgsDebugError( u
"Failed to parse Google fonts JSON, expected array."_s );
297 catch ( nlohmann::json::exception &ex )
299 QgsDebugError( u
"Failed to parse Google fonts JSON: %1"_s.arg( ex.what() ) );
310 matchedFamily.clear();
315 if ( candidate.standardizedFamily() == cleanedFamily )
317 matchedFamily = candidate.family();
337 if ( identifier.isEmpty() )
339 description = tr(
"Installing %1" ).arg( details.
family() );
343 description = tr(
"Installing %1" ).arg( identifier );
346 QgsFontDownloadTask *task =
new QgsFontDownloadTask( description, details );
347 connect( task, &QgsFontDownloadTask::taskTerminated,
this, [
this, task, identifier] {
349 mPendingFontDownloads.remove( identifier );
355 connect( task, &QgsFontDownloadTask::taskCompleted,
this, [
this, task, details, identifier] {
356 const QList<QByteArray > allFontData = task->fontData();
357 QStringList allFamilies;
358 QStringList allLicenseDetails;
360 QString errorMessage;
361 for (
int i = 0; i < allFontData.size(); ++i )
363 QStringList thisUrlFamilies;
364 const QByteArray fontData = allFontData[i];
365 const QString contentDispositionFilename = task->contentDispositionFilenames().at( i );
367 if ( contentDispositionFilename.isEmpty() )
369 const QUrl originalUrl = details.
fontUrls().value( i );
370 const thread_local QRegularExpression rxExtension( u
"^.*\\.(\\w+?)$"_s );
371 extension = rxExtension.match( originalUrl.toString() ).captured( 1 );
373 QString thisLicenseDetails;
374 if ( !
installFontsFromData( fontData, errorMessage, thisUrlFamilies, thisLicenseDetails, contentDispositionFilename, extension ) )
377 mPendingFontDownloads.remove( identifier );
385 for (
const QString &family : std::as_const( thisUrlFamilies ) )
387 if ( !allFamilies.contains( family ) )
388 allFamilies.append( family );
390 if ( !thisLicenseDetails.isEmpty() && !allLicenseDetails.contains( thisLicenseDetails ) )
392 allLicenseDetails.append( thisLicenseDetails );
397 if ( !task->licenseData().isEmpty() && !allLicenseDetails.contains( task->licenseData() ) )
399 allLicenseDetails.append( task->licenseData() );
403 mPendingFontDownloads.remove( identifier );
406 emit
fontDownloaded( allFamilies, allLicenseDetails.isEmpty() ? QString() : allLicenseDetails.join(
"\n\n" ) );
419 errorMessage.clear();
421 licenseDetails.clear();
423 QTemporaryFile tempFile;
424 if ( !extension.isEmpty() )
426 QString cleanedExtension = extension;
427 if ( cleanedExtension.startsWith(
'.' ) )
428 cleanedExtension = cleanedExtension.mid( 1 );
429 tempFile.setFileTemplate( u
"%1/XXXXXX.%2"_s.arg( QDir::tempPath(), cleanedExtension ) );
431 QTemporaryDir tempDir;
437 const QDir fontsDir( userFontsDir );
439 if ( !tempFile.open() )
441 errorMessage = tr(
"Could not write font data to a temporary file" );
445 tempFile.write( data );
448 QString sourcePath = tempFile.fileName();
451 int id = QFontDatabase::addApplicationFontFromData( data );
455 const QStringList foundFamilies = QFontDatabase::applicationFontFamilies(
id );
457 QFontDatabase::removeApplicationFont(
id );
459 if ( foundFamilies.empty() )
461 errorMessage = tr(
"Could not find any families in font" );
465 QgsDebugMsgLevel( u
"Found fonts %1"_s.arg( foundFamilies.join(
',' ) ), 2 );
466 families = foundFamilies;
468 const QString family = families.at( 0 );
469 const QString destPath = QgsFileUtils::uniquePath( fontsDir.filePath( filename.isEmpty() ? family : filename ) );
471 if ( !QFile::copy( sourcePath, destPath ) )
473 errorMessage = tr(
"Could not copy font to %1" ).arg( destPath );
477 id = QFontDatabase::addApplicationFont( destPath );
480 errorMessage = tr(
"Could not install font from %1" ).arg( destPath );
486 mUserFontToFamilyMap.insert( destPath, foundFamilies );
487 mUserFontToIdMap.insert( destPath,
id );
498 for (
const QString &file : std::as_const( files ) )
500 const QFileInfo fi( file );
501 if ( fi.fileName().compare(
"OFL.txt"_L1, Qt::CaseInsensitive ) == 0 || fi.fileName().compare(
"LICENSE.txt"_L1, Qt::CaseInsensitive ) == 0 )
503 QFile licenseFile( file );
504 if ( licenseFile.open( QIODevice::ReadOnly ) )
506 QTextStream in( &licenseFile );
507 const QString license = in.readAll();
508 licenseDetails.append( license );
511 else if ( fi.suffix().compare(
"ttf"_L1, Qt::CaseInsensitive ) == 0 || fi.suffix().compare(
"otf"_L1, Qt::CaseInsensitive ) == 0 )
514 id = QFontDatabase::addApplicationFont( sourcePath );
517 QFontDatabase::removeApplicationFont(
id );
518 const QString destPath = fontsDir.filePath( fi.fileName() );
520 if ( !QFile::exists( destPath ) && !QFile::copy( sourcePath, destPath ) )
522 errorMessage = tr(
"Could not copy font to %1" ).arg( destPath );
525 id = QFontDatabase::addApplicationFont( destPath );
528 errorMessage = tr(
"Could not install font from %1" ).arg( destPath );
531 const QStringList foundFamilies = QFontDatabase::applicationFontFamilies(
id );
532 mUserFontToFamilyMap.insert( destPath, foundFamilies );
533 mUserFontToIdMap.insert( destPath,
id );
534 for (
const QString &found : foundFamilies )
536 if ( !families.contains( found ) )
546 errorMessage = tr(
"Could not read fonts from data" );
553 if ( mUserFontDirectories.contains( directory ) )
557 mUserFontDirectories.append( directory );
560 if ( !QFile::exists( directory ) && !QDir().mkpath( directory ) )
562 QgsDebugError( u
"Cannot create local fonts dir: %1"_s.arg( directory ) );
566 installFontsFromDirectory( directory );
572 return mUserFontToFamilyMap;
578 const int id = mUserFontToIdMap.value( path, -1 );
580 QFontDatabase::removeApplicationFont(
id );
581 QFile::remove( path );
582 mUserFontToIdMap.remove( path );
583 mUserFontToFamilyMap.remove( path );
592QgsFontDownloadTask::QgsFontDownloadTask(
const QString &description,
const QgsFontDownloadDetails &details )
594 , mDetails( details )
597bool QgsFontDownloadTask::run()
599 mFeedback = std::make_unique< QgsFeedback >();
602 for (
const QString &url : mDetails.fontUrls() )
606 QNetworkRequest networkRequest( url );
608 switch ( req.
get( networkRequest,
false, mFeedback.get() ) )
628 if ( mResult && !mDetails.licenseUrl().isEmpty() )
631 QNetworkRequest networkRequest( mDetails.licenseUrl() );
633 switch ( req.
get( networkRequest,
false, mFeedback.get() ) )
644 mFailedUrl = mDetails.licenseUrl();
652void QgsFontDownloadTask::cancel()
static QString pkgDataPath()
Returns the common root path of all application data directories.
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user's home dir.
static QgsTaskManager * taskManager()
Returns the application's task manager, used for managing application wide background task handling.
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NetworkError
A network error occurred.
@ ServerExceptionError
An exception was raised by the server.
@ NoError
No error was encountered.
@ TimeoutError
Timeout was reached before a reply was received.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
Encapsulates details required for downloading a font.
static QString standardizeFamily(const QString &family)
Returns a cleaned, standardized version of a font family name.
QString licenseUrl() const
Returns the optional URL for downloading the font license details.
QStringList fontUrls() const
Returns a list of download URLs for all files associated with the font family.
QgsFontDownloadDetails()
Constructor for an invalid QgsFontDownloadDetails.
bool isValid() const
Returns true if the details represent a valid downloadable font.
QString family() const
Returns the font family.
void addFontFamilyReplacement(const QString &original, const QString &replacement)
Adds a new font replacement from the original font family to a replacement font family.
QgsFontDownloadDetails detailsForFontDownload(const QString &family, QString &matchedFamily) const
Returns a the details for downloading the specified font family.
bool removeUserFont(const QString &path)
Removes the user font at the specified path.
QMap< QString, QString > fontFamilyReplacements() const
Returns the map of automatic font family replacements.
void installUserFonts()
Installs user fonts from the profile/fonts directory as application fonts.
void fontDownloaded(const QStringList &families, const QString &licenseDetails)
Emitted when a font has downloaded and been locally loaded.
void addUserFontDirectory(const QString &directory)
Adds a directory to use for user fonts.
void setFontFamilyReplacements(const QMap< QString, QString > &replacements)
Sets the map of automatic font family replacements.
Q_DECL_DEPRECATED void downloadAndInstallFont(const QUrl &url, const QString &identifier=QString())
Downloads a font and installs in the user's profile/fonts directory as an application font,...
void enableFontDownloadsForSession()
Enables font downloads the current QGIS session.
Q_DECL_DEPRECATED QString urlForFontDownload(const QString &family, QString &matchedFamily) const
Returns the URL at which the font family can be downloaded.
static const QgsSettingsEntryStringList * settingsFontFamilyReplacements
Settings entry for font family replacements.
bool installFontsFromData(const QByteArray &data, QString &errorMessage, QStringList &families, QString &licenseDetails, const QString &filename=QString(), const QString &extension=QString())
Installs local user fonts from the specified raw data.
QString processFontFamilyName(const QString &name) const
Processes a font family name, applying any matching fontFamilyReplacements() to the name.
QMap< QString, QStringList > userFontToFamilyMap() const
Returns the mapping of installed user fonts to font families.
QgsFontManager(QObject *parent=nullptr)
Constructor for QgsFontManager, with the specified parent object.
static const QgsSettingsEntryBool * settingsDownloadMissingFonts
Settings entry for font family replacements.
bool tryToDownloadFontFamily(const QString &family, QString &matchedFamily)
Tries to download and install the specified font family.
void fontDownloadErrorOccurred(const QUrl &url, const QString &identifier, const QString &error)
Emitted when an error occurs during font downloading.
QByteArray content() const
Returns the reply content.
QByteArray rawHeader(const QByteArray &headerName) const
Returns the content of the header with the specified headerName, or an empty QByteArray if the specif...
static QString extractFileNameFromContentDispositionHeader(const QString &header)
Extracts the filename component of the content disposition header from the header.
A convenience class that simplifies locking and unlocking QReadWriteLocks.
void unlock()
Unlocks the lock.
void changeMode(Mode mode)
Change the mode of the lock to mode.
bool setValue(const T &value, const QString &dynamicKeyPart=QString()) const
Set settings value.
A boolean settings entry.
A string list settings entry.
static QgsSettingsTreeNode * sTreeFonts
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
Abstract base class for long running background tasks.
virtual void cancel()
Notifies the task that it should terminate.
static bool unzip(const QString &zip, const QString &dir, QStringList &files, bool checkConsistency=true)
Unzip a zip file in an output directory.
std::vector< QgsFontDownloadDetails > loadGoogleFontsFromJson()
QgsFontDownloadDetails GoogleFontDetails(const QString &family, const QStringList &downloadPaths, const QString &licensePath=QString())
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)