Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2026 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 : #include "fontrendercache.h"
23 :
24 : #include <array>
25 : #include <iomanip>
26 : #include <limits>
27 : #include <sstream>
28 : #include <string_view>
29 :
30 : #include "global/io/dir.h"
31 : #include "global/io/file.h"
32 : #include "global/io/fileinfo.h"
33 : #include "global/stringutils.h"
34 :
35 : #include "log.h"
36 :
37 : using namespace muse::draw;
38 :
39 23 : void FontRenderCache::init()
40 : {
41 23 : m_cache.clear();
42 23 : m_cacheInfoLoaded = false;
43 23 : m_cacheInfoMap.clear();
44 :
45 23 : if (!m_cacheDirPath.empty()) {
46 0 : muse::io::Dir::mkpath(m_cacheDirPath);
47 0 : updateCacheRevision();
48 : }
49 23 : }
50 :
51 3 : void FontRenderCache::setCacheDirPath(const muse::io::path_t& path, const std::string& revision)
52 : {
53 3 : m_cacheDirPath = path;
54 3 : m_cacheRevision = revision;
55 3 : m_cache.clear();
56 3 : m_cacheInfoLoaded = false;
57 3 : m_cacheInfoMap.clear();
58 :
59 3 : if (!m_cacheDirPath.empty()) {
60 3 : muse::io::Dir::mkpath(m_cacheDirPath);
61 3 : updateCacheRevision();
62 : }
63 3 : }
64 :
65 0 : muse::io::path_t FontRenderCache::revisionFilePath() const
66 : {
67 0 : return m_cacheDirPath.appendingComponent("revision");
68 : }
69 :
70 3 : void FontRenderCache::updateCacheRevision()
71 : {
72 3 : if (m_cacheRevision.empty()) {
73 3 : return;
74 : }
75 :
76 0 : bool isNeedClear = true;
77 0 : const muse::io::path_t revisionPath = revisionFilePath();
78 0 : if (muse::io::File::exists(revisionPath)) {
79 0 : muse::io::File revisionFile(revisionPath);
80 0 : if (revisionFile.open(muse::io::IODevice::ReadOnly)) {
81 0 : const muse::ByteArray data = revisionFile.readAll();
82 0 : isNeedClear = std::string_view(data.constChar(), data.size()) != m_cacheRevision;
83 0 : }
84 0 : }
85 :
86 0 : if (isNeedClear) {
87 0 : muse::RetVal<muse::io::paths_t> files = muse::io::Dir::scanFiles(m_cacheDirPath, {}, muse::io::ScanMode::FilesInCurrentDir);
88 0 : if (!files.ret) {
89 0 : LOGE() << "failed scan font render cache dir: " << m_cacheDirPath << ", error: " << files.ret.toString();
90 0 : return;
91 : }
92 :
93 0 : bool isCleared = true;
94 0 : for (const muse::io::path_t& filePath : files.val) {
95 0 : if (!muse::io::File::remove(filePath)) {
96 0 : LOGE() << "failed remove font render cache file: " << filePath;
97 0 : isCleared = false;
98 : }
99 : }
100 :
101 0 : m_cacheInfoLoaded = false;
102 0 : m_cacheInfoMap.clear();
103 :
104 0 : if (!isCleared) {
105 0 : return;
106 : }
107 0 : }
108 :
109 0 : if (!muse::io::File::writeFile(revisionPath, muse::ByteArray(m_cacheRevision.c_str())).success()) {
110 0 : LOGE() << "failed write cache revision file: " << revisionPath;
111 : }
112 0 : }
113 :
114 5 : static std::string realToString(double n)
115 : {
116 5 : std::ostringstream stream;
117 5 : stream << std::scientific << std::setprecision(std::numeric_limits<double>::max_digits10) << n;
118 10 : return stream.str();
119 5 : }
120 :
121 : enum class CacheInfoParam : size_t {
122 : Width = 0,
123 : Height,
124 : RectX,
125 : RectY,
126 : RectWidth,
127 : RectHeight,
128 : Range,
129 : Count
130 : };
131 :
132 : static constexpr size_t CACHE_INFO_PARAM_COUNT = static_cast<size_t>(CacheInfoParam::Count);
133 :
134 8 : static constexpr size_t paramIndex(CacheInfoParam param)
135 : {
136 8 : return static_cast<size_t>(param);
137 : }
138 :
139 3 : static std::string keyToString(const FaceKey& face, glyph_idx_t glyphIdx)
140 : {
141 3 : std::string str;
142 3 : str.reserve(50);
143 3 : str += face.dataKey.family().id().toStdString();
144 3 : str += "_" + std::to_string(glyphIdx);
145 3 : str += "_" + std::to_string(face.dataKey.bold());
146 3 : str += "_" + std::to_string(face.dataKey.italic());
147 3 : str += "_" + std::to_string(face.pixelSize);
148 3 : return str;
149 0 : }
150 :
151 1 : static std::array<std::string, CACHE_INFO_PARAM_COUNT> cacheInfoParams(const GlyphImage& image)
152 : {
153 : return {
154 1 : std::to_string(image.sdf.width),
155 1 : std::to_string(image.sdf.height),
156 : realToString(image.rect.x()),
157 : realToString(image.rect.y()),
158 : realToString(image.rect.width()),
159 : realToString(image.rect.height()),
160 1 : realToString(image.range),
161 1 : };
162 1 : }
163 :
164 1 : static std::string cacheInfoToString(const GlyphImage& image)
165 : {
166 1 : std::string str;
167 1 : const std::array<std::string, CACHE_INFO_PARAM_COUNT> params = cacheInfoParams(image);
168 16 : for (size_t i = 0; i < params.size(); ++i) {
169 7 : if (i > 0) {
170 6 : str += "|";
171 : }
172 7 : str += params.at(i);
173 : }
174 1 : return str;
175 1 : }
176 :
177 1 : muse::io::path_t FontRenderCache::makeFilePath(const FaceKey& face, glyph_idx_t glyphIdx, const GlyphImage& image) const
178 : {
179 1 : std::string str;
180 1 : str.reserve(100);
181 1 : str += keyToString(face, glyphIdx);
182 1 : str += "_[" + cacheInfoToString(image);
183 1 : str += "].sdf";
184 :
185 2 : return muse::io::path_t(str);
186 1 : }
187 :
188 1 : void FontRenderCache::store(const FaceKey& face, glyph_idx_t glyphIdx, const GlyphImage& image)
189 : {
190 1 : m_cache[face][glyphIdx] = image;
191 :
192 1 : if (m_cacheDirPath.empty()) {
193 0 : return;
194 : }
195 :
196 1 : muse::io::path_t fileFullPath = m_cacheDirPath.appendingComponent(makeFilePath(face, glyphIdx, image));
197 1 : muse::io::File file(fileFullPath);
198 1 : if (!file.open(muse::io::IODevice::WriteOnly)) {
199 0 : LOGE() << "failed open to write file: " << fileFullPath;
200 0 : return;
201 : }
202 :
203 1 : file.write(image.sdf.bitmap);
204 1 : }
205 :
206 2 : GlyphImage FontRenderCache::load(const FaceKey& face, glyph_idx_t glyphIdx) const
207 : {
208 2 : auto faceIt = m_cache.find(face);
209 2 : if (faceIt != m_cache.end()) {
210 0 : const GlyphImages& images = faceIt->second;
211 0 : auto imageIt = images.find(glyphIdx);
212 0 : if (imageIt != images.end()) {
213 0 : return imageIt->second;
214 : }
215 : }
216 :
217 2 : if (!m_cacheInfoLoaded) {
218 2 : if (!m_cacheDirPath.empty()) {
219 2 : loadCachedInfo(m_cacheInfoMap, m_cacheDirPath);
220 : }
221 2 : m_cacheInfoLoaded = true;
222 : }
223 :
224 2 : auto cacheInfoIt = m_cacheInfoMap.find(keyToString(face, glyphIdx));
225 2 : if (cacheInfoIt == m_cacheInfoMap.end()) {
226 1 : return GlyphImage();
227 : }
228 :
229 1 : const CacheInfo& info = cacheInfoIt->second;
230 1 : muse::io::File file(info.filePath);
231 1 : if (!file.open(muse::io::IODevice::ReadOnly)) {
232 0 : LOGE() << "failed read file: " << file.filePath();
233 0 : return GlyphImage();
234 : }
235 :
236 1 : GlyphImage image;
237 1 : image.sdf.bitmap = file.readAll();
238 1 : image.sdf.width = info.width;
239 1 : image.sdf.hash = std::hash<std::string_view> {}({ reinterpret_cast<const char*>(image.sdf.bitmap.data()),
240 : image.sdf.bitmap.size() });
241 1 : image.sdf.height = info.height;
242 1 : image.rect = info.rect;
243 1 : image.range = info.range;
244 :
245 1 : m_cache[face][glyphIdx] = image;
246 :
247 1 : return image;
248 1 : }
249 :
250 2 : void FontRenderCache::loadCachedInfo(CacheInfoMap& map, const muse::io::path_t& dir) const
251 : {
252 2 : muse::RetVal<muse::io::paths_t> files = muse::io::Dir::scanFiles(dir, {}, muse::io::ScanMode::FilesInCurrentDir);
253 4 : for (const muse::io::path_t& p : files.val) {
254 2 : std::string str = muse::io::FileInfo(p).completeBaseName().toStdString();
255 :
256 2 : size_t startInfoIdx = str.find('[');
257 2 : if (startInfoIdx == std::string::npos) {
258 0 : continue;
259 : }
260 :
261 2 : size_t endInfoIdx = str.find(']');
262 2 : if (endInfoIdx == std::string::npos) {
263 0 : continue;
264 : }
265 :
266 2 : std::string key = str.substr(0, startInfoIdx - 1);
267 2 : std::string data = str.substr(startInfoIdx + 1, endInfoIdx - startInfoIdx - 1);
268 2 : std::vector<std::string> params;
269 6 : muse::strings::split(data, params, "|");
270 2 : if (params.size() != CACHE_INFO_PARAM_COUNT) {
271 0 : continue;
272 : }
273 :
274 2 : bool ok = false;
275 :
276 2 : int width = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::Width))).toInt(&ok);
277 2 : if (!ok || width < 0) {
278 1 : LOGW() << "failed parse font render cache info from file: " << p;
279 1 : continue;
280 : }
281 :
282 1 : int height = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::Height))).toInt(&ok);
283 1 : if (!ok || height < 0) {
284 0 : LOGW() << "failed parse font render cache info from file: " << p;
285 0 : continue;
286 : }
287 :
288 1 : double rectX = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::RectX))).toDouble(&ok);
289 1 : bool isParsed = ok;
290 1 : double rectY = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::RectY))).toDouble(&ok);
291 1 : isParsed = isParsed && ok;
292 1 : double rectWidth = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::RectWidth))).toDouble(&ok);
293 1 : isParsed = isParsed && ok;
294 1 : double rectHeight = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::RectHeight))).toDouble(&ok);
295 1 : isParsed = isParsed && ok;
296 1 : double range = muse::AsciiStringView(params.at(paramIndex(CacheInfoParam::Range))).toDouble(&ok);
297 1 : isParsed = isParsed && ok;
298 :
299 1 : if (!isParsed) {
300 0 : LOGW() << "failed parse font render cache info from file: " << p;
301 0 : continue;
302 : }
303 :
304 1 : CacheInfo info;
305 1 : info.filePath = p;
306 1 : info.width = static_cast<uint32_t>(width);
307 1 : info.height = static_cast<uint32_t>(height);
308 1 : info.rect = RectF(rectX, rectY, rectWidth, rectHeight);
309 1 : info.range = range;
310 :
311 1 : map[key] = info;
312 5 : }
313 2 : }
|