From 365ee43613e2c88184cf4738221c56ab2c5a541f Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Thu, 22 Jun 2023 14:27:47 +0200 Subject: [PATCH] [Subtitles] Add support to font collection (.ttc) --- xbmc/guilib/GUIFontManager.cpp | 47 ++++++--- xbmc/guilib/GUIFontManager.h | 6 +- xbmc/utils/FontUtils.cpp | 180 ++++++++++++++++++++++++++------- xbmc/utils/FontUtils.h | 27 ++++- 4 files changed, 206 insertions(+), 54 deletions(-) diff --git a/xbmc/guilib/GUIFontManager.cpp b/xbmc/guilib/GUIFontManager.cpp index cf90147b01392..1e11cd1bd69ce 100644 --- a/xbmc/guilib/GUIFontManager.cpp +++ b/xbmc/guilib/GUIFontManager.cpp @@ -49,6 +49,8 @@ using namespace ADDON; namespace { constexpr const char* XML_FONTCACHE_FILENAME = "fontcache.xml"; +constexpr int XML_FONTCACHE_VER_0 = 0; // Initial version +constexpr int XML_FONTCACHE_VER_1 = 1; // Add font collection (.ttc) support to subtitles bool LoadXMLData(const std::string& filepath, CXBMCTinyXML& xmlDoc) { @@ -593,14 +595,26 @@ void GUIFontManager::LoadUserFonts() TiXmlElement* pRootElement = xmlDoc.RootElement(); if (pRootElement) { + int version{XML_FONTCACHE_VER_0}; + pRootElement->QueryIntAttribute("version", &version); + + if (version == XML_FONTCACHE_VER_0) + pRootElement->Clear(); // Reset existing data for version upgrade + const TiXmlNode* fontNode = pRootElement->FirstChild("font"); while (fontNode) { std::string filename; - std::string familyName; XMLUtils::GetString(fontNode, "filename", filename); - XMLUtils::GetString(fontNode, "familyname", familyName); - m_userFontsCache.emplace_back(filename, familyName); + + std::set familyNames; + for (const TiXmlElement* fnChildNode = fontNode->FirstChildElement("familyname"); + fnChildNode; fnChildNode = fnChildNode->NextSiblingElement("familyname")) + { + familyNames.insert(fnChildNode->GetText()); + } + + m_userFontsCache.emplace_back(filename, familyNames); fontNode = fontNode->NextSibling("font"); } } @@ -645,10 +659,10 @@ void GUIFontManager::LoadUserFonts() if (item->m_bIsFolder) continue; - std::string familyName = UTILS::FONT::GetFontFamily(filepath); - if (!familyName.empty()) + std::set familyNames; + if (UTILS::FONT::GetFontFamilyNames(filepath, familyNames)) { - m_userFontsCache.emplace_back(item->GetLabel(), familyName); + m_userFontsCache.emplace_back(item->GetLabel(), familyNames); } } isCacheChanged = isCacheChanged || previousCacheSize != m_userFontsCache.size(); @@ -659,23 +673,29 @@ void GUIFontManager::LoadUserFonts() CXBMCTinyXML xmlDoc; TiXmlDeclaration decl("1.0", "UTF-8", "yes"); xmlDoc.InsertEndChild(decl); + TiXmlElement xmlMainElement("fonts"); + xmlMainElement.SetAttribute("version", XML_FONTCACHE_VER_1); + TiXmlNode* fontsNode = xmlDoc.InsertEndChild(xmlMainElement); if (fontsNode) { - for (auto& fontMetadata : m_userFontsCache) + for (const FontMetadata& fontMetadata : m_userFontsCache) { TiXmlElement fontElement("font"); TiXmlNode* fontNode = fontsNode->InsertEndChild(fontElement); XMLUtils::SetString(fontNode, "filename", fontMetadata.m_filename); - XMLUtils::SetString(fontNode, "familyname", fontMetadata.m_familyName); + for (const std::string& familyName : fontMetadata.m_familyNames) + { + XMLUtils::SetString(fontNode, "familyname", familyName); + } } if (!xmlDoc.SaveFile(userFontCacheFilepath)) - CLog::LogF(LOGERROR, "Failed to save fonts cache file '{}'", userFontCacheFilepath); + CLog::LogF(LOGERROR, "Failed to save fonts cache file \"{}\"", userFontCacheFilepath); } else { - CLog::LogF(LOGERROR, "Failed to create XML 'fonts' node"); + CLog::LogF(LOGERROR, "Failed to create XML \"fonts\" node"); } } CLog::LogF(LOGDEBUG, "Updating user fonts cache... DONE"); @@ -687,9 +707,12 @@ std::vector GUIFontManager::GetUserFontsFamilyNames() // Duplicated family names can happens for example when a font have each style // on different files std::set familyNames; - for (auto& fontMetadata : m_userFontsCache) + for (const FontMetadata& fontMetadata : m_userFontsCache) { - familyNames.insert(fontMetadata.m_familyName); + for (const std::string& familyName : fontMetadata.m_familyNames) + { + familyNames.insert(familyName); + } } return std::vector(familyNames.begin(), familyNames.end()); } diff --git a/xbmc/guilib/GUIFontManager.h b/xbmc/guilib/GUIFontManager.h index 16924a7cbd0ea..76b64fb7450e7 100644 --- a/xbmc/guilib/GUIFontManager.h +++ b/xbmc/guilib/GUIFontManager.h @@ -44,13 +44,13 @@ struct OrigFontInfo struct FontMetadata { - FontMetadata(const std::string& filename, const std::string& familyName) - : m_filename{filename}, m_familyName{familyName} + FontMetadata(const std::string& filename, const std::set& familyNames) + : m_filename{filename}, m_familyNames{familyNames} { } std::string m_filename; - std::string m_familyName; + std::set m_familyNames; }; /*! diff --git a/xbmc/utils/FontUtils.cpp b/xbmc/utils/FontUtils.cpp index 031f0d8f41009..a5c34c9a98718 100644 --- a/xbmc/utils/FontUtils.cpp +++ b/xbmc/utils/FontUtils.cpp @@ -25,6 +25,147 @@ using namespace XFILE; +namespace +{ +// \brief Get font family from SFNT table entries +std::string GetFamilyNameFromSfnt(FT_Face face) +{ + std::string familyName; + + for (FT_UInt index = 0; index < FT_Get_Sfnt_Name_Count(face); ++index) + { + FT_SfntName name; + if (FT_Get_Sfnt_Name(face, index, &name) != 0) + { + CLog::LogF(LOGWARNING, "Failed to get SFNT name at index {}", index); + continue; + } + + // Get the unicode font family name (format-specific interface) + // In properties there may be one or more font family names encoded for each platform. + // NOTE: we give preference to MS/APPLE platform data, then fallback to MAC + // because has been found some fonts that provide names not convertible MAC text to UTF8 + if (name.name_id == TT_NAME_ID_FONT_FAMILY) + { + const std::string nameEnc{reinterpret_cast(name.string), name.string_len}; + + if (name.platform_id == TT_PLATFORM_MICROSOFT || + name.platform_id == TT_PLATFORM_APPLE_UNICODE) + { + if (name.language_id != TT_MAC_LANGID_ENGLISH && + name.language_id != TT_MS_LANGID_ENGLISH_UNITED_STATES && + name.language_id != TT_MS_LANGID_ENGLISH_UNITED_KINGDOM) + continue; + + if (CCharsetConverter::utf16BEtoUTF8(nameEnc, familyName)) + break; // Stop here to prefer the name given with this platform + else + CLog::LogF(LOGERROR, "Failed to convert the font name string encoded as \"UTF-16BE\""); + } + else if (name.platform_id == TT_PLATFORM_MACINTOSH && familyName.empty()) + { + if (name.language_id != TT_MAC_LANGID_ENGLISH || name.encoding_id != TT_MAC_ID_ROMAN) + continue; + + if (!CCharsetConverter::MacintoshToUTF8(nameEnc, familyName)) + CLog::LogF(LOGERROR, "Failed to convert the font name string encoded as \"macintosh\""); + } + else + { + CLog::LogF(LOGERROR, "Unsupported font SFNT name platform \"{}\"", name.platform_id); + } + } + } + return familyName; +} +} // unnamed namespace + +bool UTILS::FONT::GetFontFamilyNames(std::vector& buffer, + std::set& familyNames) +{ + FT_Library m_library{nullptr}; + FT_Init_FreeType(&m_library); + if (!m_library) + { + CLog::LogF(LOGERROR, "Unable to initialize freetype library"); + return false; + } + + FT_Open_Args args{}; + args.flags = FT_OPEN_MEMORY; + args.memory_base = reinterpret_cast(buffer.data()); + args.memory_size = static_cast(buffer.size()); + + FT_Long numFaces{0}; + FT_Long numInstances{0}; + FT_Long faceIndex{0}; + FT_Long instanceIndex{0}; + + // Iterate over all font faces, files like .ttc (TrueType Collection) contains multiple fonts + do + { + FT_Long idx = (instanceIndex << 16) + faceIndex; + FT_Face face{nullptr}; + + FT_Error error = FT_Open_Face(m_library, &args, idx, &face); + if (error) + { + CLog::LogF(LOGERROR, "Failed to open font face at index {} error code {}", idx, error); + break; + } + + std::string familyName = GetFamilyNameFromSfnt(face); + if (familyName.empty()) + { + CLog::LogF(LOGWARNING, "Failed to get the unicode family name for \"{}\", fallback to ASCII", + face->family_name); + // ASCII font family name may differ from the unicode one, use this as fallback only + familyName = std::string{face->family_name}; + if (familyName.empty()) + { + CLog::LogF(LOGERROR, "Cannot add font due to missing family name"); + return false; + } + } + + // We use the "set" container to avoid duplicate names that can happens + // for example when a font have each style on different files + familyNames.insert(familyName); + + numFaces = face->num_faces; + numInstances = face->style_flags >> 16; + + FT_Done_Face(face); + + if (instanceIndex < numInstances) + instanceIndex++; + else + { + faceIndex++; + instanceIndex = 0; + } + + } while (faceIndex < numFaces); + + FT_Done_FreeType(m_library); + return true; +} + +bool UTILS::FONT::GetFontFamilyNames(const std::string& filepath, + std::set& familyNames) +{ + std::vector buffer; + if (filepath.empty()) + return false; + + if (XFILE::CFile().LoadFile(filepath, buffer) <= 0) + { + CLog::LogF(LOGERROR, "Failed to load file {}", filepath); + return false; + } + return GetFontFamilyNames(buffer, familyNames); +} + std::string UTILS::FONT::GetFontFamily(std::vector& buffer) { FT_Library m_library{nullptr}; @@ -41,48 +182,15 @@ std::string UTILS::FONT::GetFontFamily(std::vector& buffer) if (FT_New_Memory_Face(m_library, reinterpret_cast(buffer.data()), buffer.size(), 0, &face) == 0) { - // Get SFNT table entries to get font properties - for (FT_UInt index = 0; index < FT_Get_Sfnt_Name_Count(face); index++) - { - FT_SfntName name; - if (FT_Get_Sfnt_Name(face, index, &name) != 0) - { - CLog::LogF(LOGWARNING, "Failed to get SFNT name at index {}", index); - continue; - } - - // Get the unicode font family name (format-specific interface) - // In properties there may be one or more font family names encoded for - // each platform, we take the first available converting it to UTF-8 - if (name.name_id == TT_NAME_ID_FONT_FAMILY) - { - const std::string nameEnc{reinterpret_cast(name.string), name.string_len}; - - if (name.platform_id == TT_PLATFORM_MICROSOFT || - name.platform_id == TT_PLATFORM_APPLE_UNICODE) - { - if (!CCharsetConverter::utf16BEtoUTF8(nameEnc, familyName)) - CLog::LogF(LOGERROR, "Failed to convert the font name string encoded as \"UTF-16BE\""); - } - else if (name.platform_id == TT_PLATFORM_MACINTOSH) - { - if (!CCharsetConverter::MacintoshToUTF8(nameEnc, familyName)) - CLog::LogF(LOGERROR, "Failed to convert the font name string encoded as \"macintosh\""); - } - else - { - CLog::LogF(LOGERROR, "Unsupported font SFNT name platform \"{}\"", name.platform_id); - } - if (!familyName.empty()) - break; - } - } + familyName = GetFamilyNameFromSfnt(face); if (familyName.empty()) { CLog::LogF(LOGWARNING, "Failed to get the unicode family name for \"{}\", fallback to ASCII", face->family_name); // ASCII font family name may differ from the unicode one, use this as fallback only familyName = std::string{face->family_name}; + if (familyName.empty()) + CLog::LogF(LOGERROR, "Missing family name from font"); } } else diff --git a/xbmc/utils/FontUtils.h b/xbmc/utils/FontUtils.h index d4b92ddcee9a7..96310a25a4293 100644 --- a/xbmc/utils/FontUtils.h +++ b/xbmc/utils/FontUtils.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -16,7 +17,7 @@ namespace UTILS { namespace FONT { -constexpr const char* SUPPORTED_EXTENSIONS_MASK = ".ttf|.otf"; +constexpr const char* SUPPORTED_EXTENSIONS_MASK = ".ttf|.ttc|.otf"; // The default application font constexpr const char* FONT_DEFAULT_FILENAME = "arial.ttf"; @@ -40,14 +41,34 @@ std::string GetSystemFontPath(const std::string& filename); }; // namespace FONTPATH /*! - * \brief Get the font family name from a font file + * \brief Get the font family name from a font file, + * in case of font collection (.ttc) will take the family name of all fonts + * \param buffer The font data + * \param familyNames The font family names + * \return True if success, otherwise false when an error occurs + */ +bool GetFontFamilyNames(std::vector& buffer, std::set& familyNames); + +/*! + * \brief Get the font family name from a font file, + * in case of font collection (.ttc) will take the family name of all fonts + * \param filepath The path where read the font data + * \param familyNames The font family names + * \return True if success, otherwise false when an error occurs + */ +bool GetFontFamilyNames(const std::string& filepath, std::set& familyNames); + +/*! + * \brief Get the font family name from a font file, + * in case of font collection (.ttc) will take the first available * \param buffer The font data * \return The font family name, otherwise empty if fails */ std::string GetFontFamily(std::vector& buffer); /*! - * \brief Get the font family name from a font file + * \brief Get the font family name from a font file, + * in case of font collection (.ttc) will take the first available * \param filepath The path where read the font data * \return The font family name, otherwise empty if fails */