Skip to content

Commit

Permalink
[Subtitles] Add support to font collection (.ttc)
Browse files Browse the repository at this point in the history
  • Loading branch information
CastagnaIT committed Jun 22, 2023
1 parent 118d69b commit 365ee43
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 54 deletions.
47 changes: 35 additions & 12 deletions xbmc/guilib/GUIFontManager.cpp
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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<std::string> 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");
}
}
Expand Down Expand Up @@ -645,10 +659,10 @@ void GUIFontManager::LoadUserFonts()
if (item->m_bIsFolder)
continue;

std::string familyName = UTILS::FONT::GetFontFamily(filepath);
if (!familyName.empty())
std::set<std::string> 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();
Expand All @@ -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");
Expand All @@ -687,9 +707,12 @@ std::vector<std::string> GUIFontManager::GetUserFontsFamilyNames()
// Duplicated family names can happens for example when a font have each style
// on different files
std::set<std::string, sortstringbyname> 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<std::string>(familyNames.begin(), familyNames.end());
}
6 changes: 3 additions & 3 deletions xbmc/guilib/GUIFontManager.h
Expand Up @@ -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<std::string>& familyNames)
: m_filename{filename}, m_familyNames{familyNames}
{
}

std::string m_filename;
std::string m_familyName;
std::set<std::string> m_familyNames;
};

/*!
Expand Down
180 changes: 144 additions & 36 deletions xbmc/utils/FontUtils.cpp
Expand Up @@ -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<const char*>(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<uint8_t>& buffer,
std::set<std::string>& 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<const FT_Byte*>(buffer.data());
args.memory_size = static_cast<FT_Long>(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<std::string>& familyNames)
{
std::vector<uint8_t> 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<uint8_t>& buffer)
{
FT_Library m_library{nullptr};
Expand All @@ -41,48 +182,15 @@ std::string UTILS::FONT::GetFontFamily(std::vector<uint8_t>& buffer)
if (FT_New_Memory_Face(m_library, reinterpret_cast<const FT_Byte*>(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<const char*>(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
Expand Down
27 changes: 24 additions & 3 deletions xbmc/utils/FontUtils.h
Expand Up @@ -8,6 +8,7 @@

#pragma once

#include <set>
#include <stdint.h>
#include <string>
#include <vector>
Expand All @@ -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";
Expand All @@ -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<uint8_t>& buffer, std::set<std::string>& 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<std::string>& 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<uint8_t>& 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
*/
Expand Down

0 comments on commit 365ee43

Please sign in to comment.