LCOV - code coverage report
Current view: top level - muse/framework/draw/internal - fontrendercache.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 72.9 % 177 129
Test Date: 2026-11-05 04:07:02

            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 : }
        

Generated by: LCOV version 2.0-1