Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2024 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "engravingfontscontroller.h"
24 :
25 : #include <QDirIterator>
26 : #include <QFontDatabase>
27 : #include <QStandardPaths>
28 :
29 : #include "log.h"
30 :
31 : using namespace mu::notation;
32 :
33 0 : void EngravingFontsController::init()
34 : {
35 0 : configuration()->userMusicFontsPathChanged().onReceive(this, [this](const muse::io::path_t&) {
36 0 : scanAllDirectories();
37 0 : });
38 :
39 0 : scanAllDirectories();
40 0 : }
41 :
42 0 : void EngravingFontsController::scanAllDirectories() const
43 : {
44 0 : engravingFonts()->clearExternalFonts();
45 :
46 : // Standard locations as described in https://w3c.github.io/smufl/latest/specification/font-metadata-locations.html
47 :
48 : // These standard location roughly match up with what the following returns, but some adjustments are needed.
49 : // QStringList globalFontsPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first(2);
50 :
51 : #if defined(Q_OS_WIN)
52 : // On Windows, the second standard location returned by Qt is %ProgramData%, but we want %CommonProgramFiles%
53 : QStringList globalFontsPaths {
54 : QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first(),
55 : qgetenv("CommonProgramFiles").replace("\\", "/")
56 : };
57 : #elif defined(Q_OS_MACOS)
58 : // MacOS is correctly handled by Qt
59 : QStringList globalFontsPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first(2);
60 : #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
61 : // On Unix systems, we want $XDG_DATA_HOME (user-specific) and $XDG_DATA_DIRS (system-wide)
62 0 : QStringList globalFontsPaths { qgetenv("XDG_DATA_HOME") };
63 0 : globalFontsPaths.append(QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS")).split(':'));
64 : #endif
65 :
66 : // The first location is the user-wide location, so we should iterate in reverse order so that
67 : // user-specific fonts can override system-wide fonts
68 0 : for (auto it = globalFontsPaths.crbegin(); it != globalFontsPaths.crend(); ++it) {
69 0 : scanDirectory(*it + "/SMuFL/Fonts", false);
70 : }
71 :
72 : // Scan MuseScore-specific "private" location last so that it can override global fonts
73 0 : muse::io::path_t userFontsPath = configuration()->userMusicFontsPath();
74 0 : if (!userFontsPath.empty()) {
75 0 : scanDirectory(userFontsPath, true);
76 : }
77 :
78 0 : engravingFonts()->loadAllFonts();
79 :
80 0 : if (uiConfiguration()) {
81 0 : QStringList musicFonts;
82 0 : for (const engraving::IEngravingFontPtr& font : engravingFonts()->fonts()) {
83 0 : musicFonts << QString::fromStdString(font->name());
84 0 : musicFonts << QString::fromStdString(font->name() + " Text");
85 0 : }
86 0 : if (!musicFonts.contains("Gootville")) {
87 0 : musicFonts << "Gootville";
88 0 : musicFonts << "Gootville Text";
89 : }
90 0 : uiConfiguration()->setNonTextFonts(musicFonts);
91 0 : }
92 0 : }
93 :
94 0 : void EngravingFontsController::scanDirectory(const muse::io::path_t& path, bool isPrivate) const
95 : {
96 : using namespace muse::draw;
97 :
98 0 : std::shared_ptr<IFontsDatabase> fdb = fontsDatabase();
99 :
100 0 : QDirIterator iterator(path.toQString(), QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable);
101 :
102 0 : while (iterator.hasNext()) {
103 0 : iterator.next();
104 0 : const QString fontDir = iterator.filePath();
105 0 : const QString fontName = iterator.fileName();
106 0 : muse::io::path_t metadataPath = fontDir + "/" + fontName + ".json";
107 0 : QFile fi(metadataPath.toQString());
108 0 : if (!fi.exists()) {
109 0 : if (!isPrivate) {
110 0 : continue;
111 : }
112 0 : metadataPath = fontDir + "/" + fontName.toLower().replace(" ", "_") + "_metadata.json";
113 0 : fi.setFileName(metadataPath.toQString());
114 0 : if (!fi.exists()) {
115 0 : metadataPath = fontDir + "/" + "metadata.json";
116 0 : fi.setFileName(metadataPath.toQString());
117 : }
118 0 : if (!fi.exists()) {
119 0 : metadataPath = muse::io::path_t();
120 0 : QDirIterator jsonFilesIterator(fontDir, { "*.json" }, QDir::Files);
121 0 : while (jsonFilesIterator.hasNext()) {
122 0 : jsonFilesIterator.next();
123 0 : metadataPath = jsonFilesIterator.filePath();
124 : }
125 0 : if (metadataPath.empty()) {
126 0 : continue;
127 : }
128 0 : }
129 : }
130 :
131 : // We assume the font name is the same as the directory name,
132 : // but maybe we should instead read the metadata.json file to get the font name
133 :
134 0 : QString fontFamily = fontName;
135 :
136 0 : if (fontName.contains(u"Text")) {
137 0 : LOGI() << "Skipping text music font: " << fontName;
138 0 : continue;
139 : }
140 :
141 0 : auto findFontPath = [this, isPrivate, fontDir](QString fontName) {
142 0 : if (isPrivate) {
143 0 : return findFontPathPrivate(fontDir, fontName);
144 : } else {
145 0 : return findFontPathGlobal(fontName);
146 : }
147 0 : };
148 :
149 0 : muse::io::path_t symbolFontPath = findFontPath(fontName);
150 0 : if (symbolFontPath.empty()) {
151 0 : QString tmp = fontName;
152 0 : symbolFontPath = findFontPath(tmp.replace(" ", ""));
153 0 : }
154 :
155 0 : if (symbolFontPath.empty()) {
156 0 : LOGE() << "Music font \"" << fontName << "\" for " << metadataPath << " not found";
157 0 : continue;
158 : }
159 :
160 0 : muse::io::path_t textFontPath = findFontPath(fontName + " Text");
161 0 : if (textFontPath.empty()) {
162 0 : QString tmp = fontName;
163 0 : textFontPath = findFontPath(tmp.replace(" ", "") + "Text");
164 0 : }
165 :
166 0 : if (textFontPath.empty()) {
167 0 : textFontPath = symbolFontPath;
168 : }
169 :
170 0 : LOGI() << "Adding custom SMuFL font: " << fontName
171 0 : << "\n symbol font path: " << symbolFontPath
172 0 : << "\n text font path: " << textFontPath
173 0 : << "\n metadata path: " << metadataPath;
174 :
175 0 : muse::String fontNameStr = muse::String::fromQString(fontName);
176 0 : engravingFonts()->addExternalFont(fontName.toStdString(), fontFamily.toStdString(), symbolFontPath, metadataPath);
177 0 : fdb->addFont(FontDataKey(fontNameStr), symbolFontPath);
178 0 : fdb->addFont(FontDataKey(fontNameStr + u" Text"), textFontPath);
179 0 : fdb->insertSubstitution(fontNameStr + u" Text", u"Leland Text");
180 0 : }
181 0 : }
182 :
183 0 : muse::io::path_t EngravingFontsController::findFontPathPrivate(const QString& metadataDir, const QString& fontName) const
184 : {
185 : // Search in the folder where the metadata lives
186 :
187 : QStringList testPaths {
188 0 : metadataDir + "/" + fontName + ".ttf",
189 0 : metadataDir + "/" + fontName + ".otf"
190 0 : };
191 0 : for (const QString& testPath : testPaths) {
192 0 : if (QFile::exists(testPath)) {
193 0 : return testPath;
194 : }
195 : }
196 0 : return muse::io::path_t();
197 0 : }
198 :
199 0 : muse::io::path_t EngravingFontsController::findFontPathGlobal(const QString& fontName) const
200 : {
201 0 : QStringList fontLocations = QStandardPaths::standardLocations(QStandardPaths::FontsLocation);
202 : #if defined(Q_OS_WIN)
203 : // Qt does not include the local fonts folder in the standard locations
204 : fontLocations << qgetenv("LocalAppData").replace("\\", "/") + "/Microsoft/Windows/Fonts";
205 : #endif
206 0 : for (const QString& dir : fontLocations) {
207 0 : QDirIterator it(dir, { "*.ttf", "*.otf" }, QDir::Files);
208 0 : while (it.hasNext()) {
209 0 : it.next();
210 0 : if (it.fileName() == fontName + ".ttf" || it.fileName() == fontName + ".otf") {
211 0 : return it.filePath();
212 : }
213 : }
214 0 : }
215 0 : return muse::io::path_t();
216 0 : }
|