diff options
Diffstat (limited to 'xbmc/guilib/GUIFontTTF.cpp')
-rw-r--r-- | xbmc/guilib/GUIFontTTF.cpp | 1166 |
1 files changed, 1166 insertions, 0 deletions
diff --git a/xbmc/guilib/GUIFontTTF.cpp b/xbmc/guilib/GUIFontTTF.cpp new file mode 100644 index 0000000..6b38fb0 --- /dev/null +++ b/xbmc/guilib/GUIFontTTF.cpp @@ -0,0 +1,1166 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIFontTTF.h" + +#include "GUIFontManager.h" +#include "ServiceBroker.h" +#include "Texture.h" +#include "URL.h" +#include "filesystem/File.h" +#include "filesystem/SpecialProtocol.h" +#include "rendering/RenderSystem.h" +#include "threads/SystemClock.h" +#include "utils/MathUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WinSystem.h" + +#include <math.h> +#include <memory> +#include <queue> +#include <utility> + +// stuff for freetype +#include <ft2build.h> +#include <harfbuzz/hb-ft.h> +#if defined(HAS_GL) || defined(HAS_GLES) +#include "utils/GLUtils.h" + +#include "system_gl.h" +#endif + +#if defined(HAS_DX) +#include "guilib/D3DResource.h" +#endif + +#ifdef TARGET_WINDOWS_STORE +#define generic GenericFromFreeTypeLibrary +#endif + +#include FT_FREETYPE_H +#include FT_GLYPH_H +#include FT_OUTLINE_H +#include FT_STROKER_H + +namespace +{ +constexpr int VERTEX_PER_GLYPH = 4; // number of vertex for each glyph +constexpr int CHARS_PER_TEXTURE_LINE = 20; // number characters to cache per texture line +constexpr int MAX_TRANSLATED_VERTEX = 32; // max number of structs CTranslatedVertices expect to use +constexpr int MAX_GLYPHS_PER_TEXT_LINE = 1024; // max number of glyphs per text line expect to use +constexpr unsigned int SPACING_BETWEEN_CHARACTERS_IN_TEXTURE = 1; +constexpr int CHAR_CHUNK = 64; // 64 chars allocated at a time (2048 bytes) +constexpr int GLYPH_STRENGTH_BOLD = 24; +constexpr int GLYPH_STRENGTH_LIGHT = -48; +constexpr int TAB_SPACE_LENGTH = 4; +} /* namespace */ + +class CFreeTypeLibrary +{ +public: + CFreeTypeLibrary() = default; + virtual ~CFreeTypeLibrary() + { + if (m_library) + FT_Done_FreeType(m_library); + } + + FT_Face GetFont(const std::string& filename, + float size, + float aspect, + std::vector<uint8_t>& memoryBuf) + { + // don't have it yet - create it + if (!m_library) + FT_Init_FreeType(&m_library); + if (!m_library) + { + CLog::LogF(LOGERROR, "Unable to initialize freetype library"); + return nullptr; + } + + FT_Face face; + + // ok, now load the font face + CURL realFile(CSpecialProtocol::TranslatePath(filename)); + if (realFile.GetFileName().empty()) + return nullptr; + + memoryBuf.clear(); +#ifndef TARGET_WINDOWS + if (!realFile.GetProtocol().empty()) +#endif // ! TARGET_WINDOWS + { + // load file into memory if it is not on local drive + // in case of win32: always load file into memory as filename is in UTF-8, + // but freetype expect filename in ANSI encoding + XFILE::CFile f; + if (f.LoadFile(realFile, memoryBuf) <= 0) + return nullptr; + + if (FT_New_Memory_Face(m_library, reinterpret_cast<const FT_Byte*>(memoryBuf.data()), + memoryBuf.size(), 0, &face) != 0) + return nullptr; + } +#ifndef TARGET_WINDOWS + else if (FT_New_Face(m_library, realFile.GetFileName().c_str(), 0, &face)) + return nullptr; +#endif // ! TARGET_WINDOWS + + unsigned int ydpi = 72; // 72 points to the inch is the freetype default + unsigned int xdpi = + static_cast<unsigned int>(MathUtils::round_int(static_cast<double>(ydpi * aspect))); + + // we set our screen res currently to 96dpi in both directions (windows default) + // we cache our characters (for rendering speed) so it's probably + // not a good idea to allow free scaling of fonts - rather, just + // scaling to pixel ratio on screen perhaps? + if (FT_Set_Char_Size(face, 0, static_cast<int>(size * 64 + 0.5f), xdpi, ydpi)) + { + FT_Done_Face(face); + return nullptr; + } + + return face; + }; + + FT_Stroker GetStroker() + { + if (!m_library) + return nullptr; + + FT_Stroker stroker; + if (FT_Stroker_New(m_library, &stroker)) + return nullptr; + + return stroker; + }; + + static void ReleaseFont(FT_Face face) + { + assert(face); + FT_Done_Face(face); + }; + + static void ReleaseStroker(FT_Stroker stroker) + { + assert(stroker); + FT_Stroker_Done(stroker); + } + +private: + FT_Library m_library{nullptr}; +}; + +XBMC_GLOBAL_REF(CFreeTypeLibrary, g_freeTypeLibrary); // our freetype library +#define g_freeTypeLibrary XBMC_GLOBAL_USE(CFreeTypeLibrary) + +CGUIFontTTF::CGUIFontTTF(const std::string& fontIdent) + : m_fontIdent(fontIdent), + m_staticCache(*this), + m_dynamicCache(*this), + m_renderSystem(CServiceBroker::GetRenderSystem()) +{ +} + +CGUIFontTTF::~CGUIFontTTF(void) +{ + Clear(); +} + +void CGUIFontTTF::AddReference() +{ + m_referenceCount++; +} + +void CGUIFontTTF::RemoveReference() +{ + // delete this object when it's reference count hits zero + m_referenceCount--; + if (!m_referenceCount) + g_fontManager.FreeFontFile(this); +} + + +void CGUIFontTTF::ClearCharacterCache() +{ + m_texture.reset(); + + DeleteHardwareTexture(); + + m_texture = nullptr; + m_char.clear(); + m_char.reserve(CHAR_CHUNK); + memset(m_charquick, 0, sizeof(m_charquick)); + // set the posX and posY so that our texture will be created on first character write. + m_posX = m_textureWidth; + m_posY = -static_cast<int>(GetTextureLineHeight()); + m_textureHeight = 0; +} + +void CGUIFontTTF::Clear() +{ + m_texture.reset(); + m_texture = nullptr; + memset(m_charquick, 0, sizeof(m_charquick)); + m_posX = 0; + m_posY = 0; + m_nestedBeginCount = 0; + + if (m_hbFont) + hb_font_destroy(m_hbFont); + m_hbFont = nullptr; + if (m_face) + g_freeTypeLibrary.ReleaseFont(m_face); + m_face = nullptr; + if (m_stroker) + g_freeTypeLibrary.ReleaseStroker(m_stroker); + m_stroker = nullptr; + + m_vertexTrans.clear(); + m_vertex.clear(); + + m_fontFileInMemory.clear(); +} + +bool CGUIFontTTF::Load( + const std::string& strFilename, float height, float aspect, float lineSpacing, bool border) +{ + // we now know that this object is unique - only the GUIFont objects are non-unique, so no need + // for reference tracking these fonts + m_face = g_freeTypeLibrary.GetFont(strFilename, height, aspect, m_fontFileInMemory); + if (!m_face) + return false; + + m_hbFont = hb_ft_font_create(m_face, 0); + if (!m_hbFont) + return false; + /* + the values used are described below + + XBMC coords Freetype coords + + 0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ bbox.yMax, ascender + A \ + A A | + A A | + AAAAA pppp cellAscender + A A p p | + A A p p | + m_cellBaseLine _ _A_ _A_ pppp_ _ _ _ _/_ _ _ _ _ 0, base line. + p \ + p cellDescender + m_cellHeight _ _ _ _ _ p _ _ _ _ _ _/_ _ _ _ _ bbox.yMin, descender + + */ + int cellDescender = std::min<int>(m_face->bbox.yMin, m_face->descender); + int cellAscender = std::max<int>(m_face->bbox.yMax, m_face->ascender); + + if (border) + { + /* + add on the strength of any border - the non-bordered font needs + aligning with the bordered font by utilising GetTextBaseLine() + */ + FT_Pos strength = FT_MulFix(m_face->units_per_EM, m_face->size->metrics.y_scale) / 12; + if (strength < 128) + strength = 128; + + cellDescender -= strength; + cellAscender += strength; + + m_stroker = g_freeTypeLibrary.GetStroker(); + if (m_stroker) + FT_Stroker_Set(m_stroker, strength, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + } + + // scale to pixel sizing, rounding so that maximal extent is obtained + float scaler = height / m_face->units_per_EM; + cellDescender = + MathUtils::round_int(cellDescender * static_cast<double>(scaler) - 0.5); // round down + cellAscender = MathUtils::round_int(cellAscender * static_cast<double>(scaler) + 0.5); // round up + + m_cellBaseLine = cellAscender; + m_cellHeight = cellAscender - cellDescender; + + m_height = height; + + m_texture.reset(); + m_texture = nullptr; + + m_textureHeight = 0; + m_textureWidth = ((m_cellHeight * CHARS_PER_TEXTURE_LINE) & ~63) + 64; + + m_textureWidth = CTexture::PadPow2(m_textureWidth); + + if (m_textureWidth > m_renderSystem->GetMaxTextureSize()) + m_textureWidth = m_renderSystem->GetMaxTextureSize(); + m_textureScaleX = 1.0f / m_textureWidth; + + // set the posX and posY so that our texture will be created on first character write. + m_posX = m_textureWidth; + m_posY = -static_cast<int>(GetTextureLineHeight()); + + return true; +} + +void CGUIFontTTF::Begin() +{ + if (m_nestedBeginCount == 0 && m_texture && FirstBegin()) + { + m_vertexTrans.clear(); + m_vertex.clear(); + } + // Keep track of the nested begin/end calls. + m_nestedBeginCount++; +} + +void CGUIFontTTF::End() +{ + if (m_nestedBeginCount == 0) + return; + + if (--m_nestedBeginCount > 0) + return; + + LastEnd(); +} + +void CGUIFontTTF::DrawTextInternal(CGraphicContext& context, + float x, + float y, + const std::vector<UTILS::COLOR::Color>& colors, + const vecText& text, + uint32_t alignment, + float maxPixelWidth, + bool scrolling) +{ + if (text.empty()) + { + return; + } + + Begin(); + uint32_t rawAlignment = alignment; + bool dirtyCache(false); + const bool hardwareClipping = m_renderSystem->ScissorsCanEffectClipping(); + CGUIFontCacheStaticPosition staticPos(x, y); + CGUIFontCacheDynamicPosition dynamicPos; + if (hardwareClipping) + { + dynamicPos = + CGUIFontCacheDynamicPosition(context.ScaleFinalXCoord(x, y), context.ScaleFinalYCoord(x, y), + context.ScaleFinalZCoord(x, y)); + } + CVertexBuffer unusedVertexBuffer; + CVertexBuffer& vertexBuffer = + hardwareClipping + ? m_dynamicCache.Lookup(context, dynamicPos, colors, text, alignment, maxPixelWidth, + scrolling, std::chrono::steady_clock::now(), dirtyCache) + : unusedVertexBuffer; + std::shared_ptr<std::vector<SVertex>> tempVertices = std::make_shared<std::vector<SVertex>>(); + std::shared_ptr<std::vector<SVertex>>& vertices = + hardwareClipping ? tempVertices + : static_cast<std::shared_ptr<std::vector<SVertex>>&>(m_staticCache.Lookup( + context, staticPos, colors, text, alignment, maxPixelWidth, scrolling, + std::chrono::steady_clock::now(), dirtyCache)); + + // reserves vertex vector capacity, only the ones that are going to be used + if (hardwareClipping) + { + if (m_vertexTrans.capacity() == 0) + m_vertexTrans.reserve(MAX_TRANSLATED_VERTEX); + } + else + { + if (m_vertex.capacity() == 0) + m_vertex.reserve(VERTEX_PER_GLYPH * MAX_GLYPHS_PER_TEXT_LINE); + } + + if (dirtyCache) + { + const std::vector<Glyph> glyphs = GetHarfBuzzShapedGlyphs(text); + // save the origin, which is scaled separately + m_originX = x; + m_originY = y; + + // cache the ellipses width + if (!m_ellipseCached) + { + m_ellipseCached = true; + Character* ellipse = GetCharacter(L'.', 0); + if (ellipse) + m_ellipsesWidth = ellipse->m_advance; + } + + // Check if we will really need to truncate or justify the text + if (alignment & XBFONT_TRUNCATED) + { + if (maxPixelWidth <= 0.0f || GetTextWidthInternal(text, glyphs) <= maxPixelWidth) + alignment &= ~XBFONT_TRUNCATED; + } + else if (alignment & XBFONT_JUSTIFIED) + { + if (maxPixelWidth <= 0.0f) + alignment &= ~XBFONT_JUSTIFIED; + } + + // calculate sizing information + float startX = 0; + float startY = (alignment & XBFONT_CENTER_Y) ? -0.5f * m_cellHeight : 0; // vertical centering + + if (alignment & (XBFONT_RIGHT | XBFONT_CENTER_X)) + { + // Get the extent of this line + float w = GetTextWidthInternal(text, glyphs); + + if (alignment & XBFONT_TRUNCATED && w > maxPixelWidth + 0.5f) // + 0.5f due to rounding issues + w = maxPixelWidth; + + if (alignment & XBFONT_CENTER_X) + w *= 0.5f; + // Offset this line's starting position + startX -= w; + } + + float spacePerSpaceCharacter = 0; // for justification effects + if (alignment & XBFONT_JUSTIFIED) + { + // first compute the size of the text to render in both characters and pixels + unsigned int numSpaces = 0; + float linePixels = 0; + for (const auto& glyph : glyphs) + { + Character* ch = GetCharacter(text[glyph.m_glyphInfo.cluster], glyph.m_glyphInfo.codepoint); + if (ch) + { + if ((text[glyph.m_glyphInfo.cluster] & 0xffff) == L' ') + numSpaces += 1; + linePixels += ch->m_advance; + } + } + if (numSpaces > 0) + spacePerSpaceCharacter = (maxPixelWidth - linePixels) / numSpaces; + } + + float cursorX = 0; // current position along the line + float offsetX = 0; + float offsetY = 0; + + // Collect all the Character info in a first pass, in case any of them + // are not currently cached and cause the texture to be enlarged, which + // would invalidate the texture coordinates. + std::queue<Character> characters; + if (alignment & XBFONT_TRUNCATED) + GetCharacter(L'.', 0); + for (const auto& glyph : glyphs) + { + Character* ch = GetCharacter(text[glyph.m_glyphInfo.cluster], glyph.m_glyphInfo.codepoint); + if (!ch) + { + Character null = {}; + characters.push(null); + continue; + } + characters.push(*ch); + + if (maxPixelWidth > 0 && + cursorX + ((alignment & XBFONT_TRUNCATED) ? ch->m_advance + 3 * m_ellipsesWidth : 0) > + maxPixelWidth) + break; + cursorX += ch->m_advance; + } + + // Reserve vector space: 4 vertex for each glyph + tempVertices->reserve(VERTEX_PER_GLYPH * glyphs.size()); + cursorX = 0; + + for (const auto& glyph : glyphs) + { + // If starting text on a new line, determine justification effects + // Get the current letter in the CStdString + UTILS::COLOR::Color color = (text[glyph.m_glyphInfo.cluster] & 0xff0000) >> 16; + if (color >= colors.size()) + color = 0; + color = colors[color]; + + // grab the next character + Character* ch = &characters.front(); + + if ((text[glyph.m_glyphInfo.cluster] & 0xffff) == static_cast<character_t>('\t')) + { + const float tabwidth = GetTabSpaceLength(); + const float a = cursorX / tabwidth; + cursorX += tabwidth - ((a - floorf(a)) * tabwidth); + characters.pop(); + continue; + } + + if (alignment & XBFONT_TRUNCATED) + { + // Check if we will be exceeded the max allowed width + if (cursorX + ch->m_advance + 3 * m_ellipsesWidth > maxPixelWidth) + { + // Yup. Let's draw the ellipses, then bail + // Perhaps we should really bail to the next line in this case?? + Character* period = GetCharacter(L'.', 0); + if (!period) + break; + + for (int i = 0; i < 3; i++) + { + RenderCharacter(context, startX + cursorX, startY, period, color, !scrolling, + *tempVertices); + cursorX += period->m_advance; + } + break; + } + } + else if (maxPixelWidth > 0 && cursorX > maxPixelWidth) + break; // exceeded max allowed width - stop rendering + + offsetX = static_cast<float>( + MathUtils::round_int(static_cast<double>(glyph.m_glyphPosition.x_offset) / 64)); + offsetY = static_cast<float>( + MathUtils::round_int(static_cast<double>(glyph.m_glyphPosition.y_offset) / 64)); + RenderCharacter(context, startX + cursorX + offsetX, startY - offsetY, ch, color, !scrolling, + *tempVertices); + if (alignment & XBFONT_JUSTIFIED) + { + if ((text[glyph.m_glyphInfo.cluster] & 0xffff) == L' ') + cursorX += ch->m_advance + spacePerSpaceCharacter; + else + cursorX += ch->m_advance; + } + else + cursorX += ch->m_advance; + characters.pop(); + } + if (hardwareClipping) + { + CVertexBuffer& vertexBuffer = + m_dynamicCache.Lookup(context, dynamicPos, colors, text, rawAlignment, maxPixelWidth, + scrolling, std::chrono::steady_clock::now(), dirtyCache); + CVertexBuffer newVertexBuffer = CreateVertexBuffer(*tempVertices); + vertexBuffer = newVertexBuffer; + m_vertexTrans.emplace_back(.0f, .0f, .0f, &vertexBuffer, context.GetClipRegion()); + } + else + { + m_staticCache.Lookup(context, staticPos, colors, text, rawAlignment, maxPixelWidth, scrolling, + std::chrono::steady_clock::now(), dirtyCache) = + *static_cast<CGUIFontCacheStaticValue*>(&tempVertices); + /* Append the new vertices to the set collected since the first Begin() call */ + m_vertex.insert(m_vertex.end(), tempVertices->begin(), tempVertices->end()); + } + } + else + { + if (hardwareClipping) + m_vertexTrans.emplace_back(dynamicPos.m_x, dynamicPos.m_y, dynamicPos.m_z, &vertexBuffer, + context.GetClipRegion()); + else + /* Append the vertices from the cache to the set collected since the first Begin() call */ + m_vertex.insert(m_vertex.end(), vertices->begin(), vertices->end()); + } + + End(); +} + + +float CGUIFontTTF::GetTextWidthInternal(const vecText& text) +{ + const std::vector<Glyph> glyphs = GetHarfBuzzShapedGlyphs(text); + return GetTextWidthInternal(text, glyphs); +} + +// this routine assumes a single line (i.e. it was called from GUITextLayout) +float CGUIFontTTF::GetTextWidthInternal(const vecText& text, const std::vector<Glyph>& glyphs) +{ + float width = 0; + for (auto it = glyphs.begin(); it != glyphs.end(); it++) + { + const character_t ch = text[(*it).m_glyphInfo.cluster]; + Character* c = GetCharacter(ch, (*it).m_glyphInfo.codepoint); + if (c) + { + // If last character in line, we want to add render width + // and not advance distance - this makes sure that italic text isn't + // choped on the end (as render width is larger than advance then). + if (std::next(it) == glyphs.end()) + width += std::max(c->m_right - c->m_left + c->m_offsetX, c->m_advance); + else if ((ch & 0xffff) == static_cast<character_t>('\t')) + width += GetTabSpaceLength(); + else + width += c->m_advance; + } + } + + return width; +} + +float CGUIFontTTF::GetCharWidthInternal(character_t ch) +{ + Character* c = GetCharacter(ch, 0); + if (c) + { + if ((ch & 0xffff) == static_cast<character_t>('\t')) + return GetTabSpaceLength(); + else + return c->m_advance; + } + + return 0; +} + +float CGUIFontTTF::GetTextHeight(float lineSpacing, int numLines) const +{ + return static_cast<float>(numLines - 1) * GetLineHeight(lineSpacing) + m_cellHeight; +} + +float CGUIFontTTF::GetLineHeight(float lineSpacing) const +{ + if (!m_face) + return 0.0f; + + return lineSpacing * m_face->size->metrics.height / 64.0f; +} + +unsigned int CGUIFontTTF::GetTextureLineHeight() const +{ + return m_cellHeight + SPACING_BETWEEN_CHARACTERS_IN_TEXTURE; +} + +unsigned int CGUIFontTTF::GetMaxFontHeight() const +{ + return m_maxFontHeight + SPACING_BETWEEN_CHARACTERS_IN_TEXTURE; +} + +std::vector<CGUIFontTTF::Glyph> CGUIFontTTF::GetHarfBuzzShapedGlyphs(const vecText& text) +{ + std::vector<Glyph> glyphs; + if (text.empty()) + { + return glyphs; + } + + std::vector<hb_script_t> scripts; + std::vector<RunInfo> runs; + hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default(); + hb_script_t lastScript; + int lastScriptIndex = -1; + int lastSetIndex = -1; + + for (const auto& character : text) + { + scripts.emplace_back(hb_unicode_script(ufuncs, static_cast<wchar_t>(0xffff & character))); + } + + // HB_SCRIPT_COMMON or HB_SCRIPT_INHERITED should be replaced with previous script + for (size_t i = 0; i < scripts.size(); ++i) + { + if (scripts[i] == HB_SCRIPT_COMMON || scripts[i] == HB_SCRIPT_INHERITED) + { + if (lastScriptIndex != -1) + { + scripts[i] = lastScript; + lastSetIndex = i; + } + } + else + { + for (size_t j = lastSetIndex + 1; j < i; ++j) + scripts[j] = scripts[i]; + lastScript = scripts[i]; + lastScriptIndex = i; + lastSetIndex = i; + } + } + + lastScript = scripts[0]; + int lastRunStart = 0; + + for (unsigned int i = 0; i <= static_cast<unsigned int>(scripts.size()); ++i) + { + if (i == scripts.size() || scripts[i] != lastScript) + { + RunInfo run{}; + run.m_startOffset = lastRunStart; + run.m_endOffset = i; + run.m_script = lastScript; + runs.emplace_back(run); + + if (i < scripts.size()) + { + lastScript = scripts[i]; + lastRunStart = i; + } + else + { + break; + } + } + } + + for (auto& run : runs) + { + run.m_buffer = hb_buffer_create(); + hb_buffer_set_direction(run.m_buffer, static_cast<hb_direction_t>(HB_DIRECTION_LTR)); + hb_buffer_set_script(run.m_buffer, run.m_script); + + for (unsigned int j = run.m_startOffset; j < run.m_endOffset; j++) + { + hb_buffer_add(run.m_buffer, static_cast<wchar_t>(0xffff & text[j]), j); + } + + hb_buffer_set_content_type(run.m_buffer, HB_BUFFER_CONTENT_TYPE_UNICODE); + hb_shape(m_hbFont, run.m_buffer, nullptr, 0); + unsigned int glyphCount; + run.m_glyphInfos = hb_buffer_get_glyph_infos(run.m_buffer, &glyphCount); + run.m_glyphPositions = hb_buffer_get_glyph_positions(run.m_buffer, &glyphCount); + + for (size_t k = 0; k < glyphCount; k++) + { + glyphs.emplace_back(run.m_glyphInfos[k], run.m_glyphPositions[k]); + } + + hb_buffer_destroy(run.m_buffer); + } + + return glyphs; +} + +CGUIFontTTF::Character* CGUIFontTTF::GetCharacter(character_t chr, FT_UInt glyphIndex) +{ + const wchar_t letter = static_cast<wchar_t>(chr & 0xffff); + + // ignore linebreaks + if (letter == L'\r') + return nullptr; + + const character_t style = (chr & 0x7000000) >> 24; // style = 0 - 6 + + if (!glyphIndex) + glyphIndex = FT_Get_Char_Index(m_face, letter); + + // quick access to the most frequently used glyphs + if (glyphIndex < MAX_GLYPH_IDX) + { + character_t ch = (style << 12) | glyphIndex; // 2^12 = 4096 + + if (ch < LOOKUPTABLE_SIZE && m_charquick[ch]) + return m_charquick[ch]; + } + + // letters are stored based on style and glyph + character_t ch = (style << 16) | glyphIndex; + + // perform binary search on sorted array by m_glyphAndStyle and + // if not found obtains position to insert the new m_char to keep sorted + int low = 0; + int high = m_char.size() - 1; + while (low <= high) + { + int mid = (low + high) >> 1; + if (ch > m_char[mid].m_glyphAndStyle) + low = mid + 1; + else if (ch < m_char[mid].m_glyphAndStyle) + high = mid - 1; + else + return &m_char[mid]; + } + // if we get to here, then low is where we should insert the new character + + int startIndex = low; + + // increase the size of the buffer if we need it + if (m_char.size() == m_char.capacity()) + { + m_char.reserve(m_char.capacity() + CHAR_CHUNK); + startIndex = 0; + } + + // render the character to our texture + // must End() as we can't render text to our texture during a Begin(), End() block + unsigned int nestedBeginCount = m_nestedBeginCount; + m_nestedBeginCount = 1; + if (nestedBeginCount) + End(); + + m_char.emplace(m_char.begin() + low); + if (!CacheCharacter(glyphIndex, style, m_char.data() + low)) + { // unable to cache character - try clearing them all out and starting over + CLog::LogF(LOGDEBUG, "Unable to cache character. Clearing character cache of {} characters", + m_char.size()); + ClearCharacterCache(); + low = 0; + startIndex = 0; + m_char.emplace(m_char.begin()); + if (!CacheCharacter(glyphIndex, style, m_char.data())) + { + CLog::LogF(LOGERROR, "Unable to cache character (out of memory?)"); + if (nestedBeginCount) + Begin(); + m_nestedBeginCount = nestedBeginCount; + return nullptr; + } + } + + if (nestedBeginCount) + Begin(); + m_nestedBeginCount = nestedBeginCount; + + // update the lookup table with only the m_char addresses that have changed + for (size_t i = startIndex; i < m_char.size(); ++i) + { + if (m_char[i].m_glyphIndex < MAX_GLYPH_IDX) + { + // >> 16 is style (0-6), then 16 - 12 (>> 4) is equivalent to style * 4096 + character_t ch = ((m_char[i].m_glyphAndStyle & 0xffff0000) >> 4) | m_char[i].m_glyphIndex; + + if (ch < LOOKUPTABLE_SIZE) + m_charquick[ch] = m_char.data() + i; + } + } + + return m_char.data() + low; +} + +bool CGUIFontTTF::CacheCharacter(FT_UInt glyphIndex, uint32_t style, Character* ch) +{ + FT_Glyph glyph = nullptr; + if (FT_Load_Glyph(m_face, glyphIndex, FT_LOAD_TARGET_LIGHT)) + { + CLog::LogF(LOGDEBUG, "Failed to load glyph {:x}", glyphIndex); + return false; + } + + // make bold if applicable + if (style & FONT_STYLE_BOLD) + SetGlyphStrength(m_face->glyph, GLYPH_STRENGTH_BOLD); + // and italics if applicable + if (style & FONT_STYLE_ITALICS) + ObliqueGlyph(m_face->glyph); + // and light if applicable + if (style & FONT_STYLE_LIGHT) + SetGlyphStrength(m_face->glyph, GLYPH_STRENGTH_LIGHT); + // grab the glyph + if (FT_Get_Glyph(m_face->glyph, &glyph)) + { + CLog::LogF(LOGDEBUG, "Failed to get glyph {:x}", glyphIndex); + return false; + } + if (m_stroker) + FT_Glyph_StrokeBorder(&glyph, m_stroker, 0, 1); + // render the glyph + if (FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, nullptr, 1)) + { + CLog::LogF(LOGDEBUG, "Failed to render glyph {:x} to a bitmap", glyphIndex); + return false; + } + + FT_BitmapGlyph bitGlyph = (FT_BitmapGlyph)glyph; + FT_Bitmap bitmap = bitGlyph->bitmap; + bool isEmptyGlyph = (bitmap.width == 0 || bitmap.rows == 0); + + if (!isEmptyGlyph) + { + if (bitGlyph->left < 0) + m_posX += -bitGlyph->left; + + // check we have enough room for the character. + // cast-fest is here to avoid warnings due to freeetype version differences (signedness of width). + if (static_cast<int>(m_posX + bitGlyph->left + bitmap.width + + SPACING_BETWEEN_CHARACTERS_IN_TEXTURE) > static_cast<int>(m_textureWidth)) + { // no space - gotta drop to the next line (which means creating a new texture and copying it across) + m_posX = 1; + m_posY += GetTextureLineHeight(); + if (bitGlyph->left < 0) + m_posX += -bitGlyph->left; + + if (m_posY + GetTextureLineHeight() >= m_textureHeight) + { + // create the new larger texture + unsigned int newHeight = m_posY + GetTextureLineHeight(); + // check for max height + if (newHeight > m_renderSystem->GetMaxTextureSize()) + { + CLog::LogF(LOGDEBUG, "New cache texture is too large ({} > {} pixels long)", newHeight, + m_renderSystem->GetMaxTextureSize()); + FT_Done_Glyph(glyph); + return false; + } + + std::unique_ptr<CTexture> newTexture = ReallocTexture(newHeight); + if (!newTexture) + { + FT_Done_Glyph(glyph); + CLog::LogF(LOGDEBUG, "Failed to allocate new texture of height {}", newHeight); + return false; + } + m_texture = std::move(newTexture); + } + m_posY = GetMaxFontHeight(); + } + + if (!m_texture) + { + FT_Done_Glyph(glyph); + CLog::LogF(LOGDEBUG, "no texture to cache character to"); + return false; + } + } + + // set the character in our table + ch->m_glyphAndStyle = (style << 16) | glyphIndex; + ch->m_glyphIndex = glyphIndex; + ch->m_offsetX = static_cast<short>(bitGlyph->left); + ch->m_offsetY = static_cast<short>(m_cellBaseLine - bitGlyph->top); + ch->m_left = isEmptyGlyph ? 0.0f : (static_cast<float>(m_posX)); + ch->m_top = isEmptyGlyph ? 0.0f : (static_cast<float>(m_posY)); + ch->m_right = ch->m_left + bitmap.width; + ch->m_bottom = ch->m_top + bitmap.rows; + ch->m_advance = + static_cast<float>(MathUtils::round_int(static_cast<double>(m_face->glyph->advance.x) / 64)); + + // we need only render if we actually have some pixels + if (!isEmptyGlyph) + { + // ensure our rect will stay inside the texture (it *should* but we need to be certain) + unsigned int x1 = std::max(m_posX, 0); + unsigned int y1 = std::max(m_posY, 0); + unsigned int x2 = std::min(x1 + bitmap.width, m_textureWidth); + unsigned int y2 = std::min(y1 + bitmap.rows, m_textureHeight); + m_maxFontHeight = std::max(m_maxFontHeight, y2); + CopyCharToTexture(bitGlyph, x1, y1, x2, y2); + + m_posX += SPACING_BETWEEN_CHARACTERS_IN_TEXTURE + + static_cast<unsigned short>(ch->m_right - ch->m_left); + } + + // free the glyph + FT_Done_Glyph(glyph); + + return true; +} + +void CGUIFontTTF::RenderCharacter(CGraphicContext& context, + float posX, + float posY, + const Character* ch, + UTILS::COLOR::Color color, + bool roundX, + std::vector<SVertex>& vertices) +{ + // actual image width isn't same as the character width as that is + // just baseline width and height should include the descent + const float width = ch->m_right - ch->m_left; + const float height = ch->m_bottom - ch->m_top; + + // return early if nothing to render + if (width == 0 || height == 0) + return; + + // posX and posY are relative to our origin, and the textcell is offset + // from our (posX, posY). Plus, these are unscaled quantities compared to the underlying GUI resolution + CRect vertex((posX + ch->m_offsetX) * context.GetGUIScaleX(), + (posY + ch->m_offsetY) * context.GetGUIScaleY(), + (posX + ch->m_offsetX + width) * context.GetGUIScaleX(), + (posY + ch->m_offsetY + height) * context.GetGUIScaleY()); + vertex += CPoint(m_originX, m_originY); + CRect texture(ch->m_left, ch->m_top, ch->m_right, ch->m_bottom); + if (!m_renderSystem->ScissorsCanEffectClipping()) + context.ClipRect(vertex, texture); + + // transform our positions - note, no scaling due to GUI calibration/resolution occurs + float x[VERTEX_PER_GLYPH] = {context.ScaleFinalXCoord(vertex.x1, vertex.y1), + context.ScaleFinalXCoord(vertex.x2, vertex.y1), + context.ScaleFinalXCoord(vertex.x2, vertex.y2), + context.ScaleFinalXCoord(vertex.x1, vertex.y2)}; + + if (roundX) + { + // We only round the "left" side of the character, and then use the direction of rounding to + // move the "right" side of the character. This ensures that a constant width is kept when rendering + // the same letter at the same size at different places of the screen, avoiding the problem + // of the "left" side rounding one way while the "right" side rounds the other way, thus getting + // altering the width of thin characters substantially. This only really works for positive + // coordinates (due to the direction of truncation for negatives) but this is the only case that + // really interests us anyway. + float rx0 = static_cast<float>(MathUtils::round_int(static_cast<double>(x[0]))); + float rx3 = static_cast<float>(MathUtils::round_int(static_cast<double>(x[3]))); + x[1] = static_cast<float>(MathUtils::truncate_int(static_cast<double>(x[1]))); + x[2] = static_cast<float>(MathUtils::truncate_int(static_cast<double>(x[2]))); + if (x[0] > 0.0f && rx0 > x[0]) + x[1] += 1; + else if (x[0] < 0.0f && rx0 < x[0]) + x[1] -= 1; + if (x[3] > 0.0f && rx3 > x[3]) + x[2] += 1; + else if (x[3] < 0.0f && rx3 < x[3]) + x[2] -= 1; + x[0] = rx0; + x[3] = rx3; + } + + const float y[VERTEX_PER_GLYPH] = { + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalYCoord(vertex.x1, vertex.y1)))), + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalYCoord(vertex.x2, vertex.y1)))), + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalYCoord(vertex.x2, vertex.y2)))), + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalYCoord(vertex.x1, vertex.y2))))}; + + const float z[VERTEX_PER_GLYPH] = { + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalZCoord(vertex.x1, vertex.y1)))), + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalZCoord(vertex.x2, vertex.y1)))), + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalZCoord(vertex.x2, vertex.y2)))), + static_cast<float>(MathUtils::round_int( + static_cast<double>(context.ScaleFinalZCoord(vertex.x1, vertex.y2))))}; + + // tex coords converted to 0..1 range + const float tl = texture.x1 * m_textureScaleX; + const float tr = texture.x2 * m_textureScaleX; + const float tt = texture.y1 * m_textureScaleY; + const float tb = texture.y2 * m_textureScaleY; + + vertices.resize(vertices.size() + VERTEX_PER_GLYPH); + SVertex* v = &vertices[vertices.size() - VERTEX_PER_GLYPH]; + m_color = color; + +#if !defined(HAS_DX) + uint8_t r = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color); + uint8_t g = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color); + uint8_t b = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color); + uint8_t a = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color); +#endif + + for (int i = 0; i < VERTEX_PER_GLYPH; i++) + { +#ifdef HAS_DX + CD3DHelper::XMStoreColor(&v[i].col, color); +#else + v[i].r = r; + v[i].g = g; + v[i].b = b; + v[i].a = a; +#endif + } + +#if defined(HAS_DX) + for (int i = 0; i < VERTEX_PER_GLYPH; i++) + { + v[i].x = x[i]; + v[i].y = y[i]; + v[i].z = z[i]; + } + + v[0].u = tl; + v[0].v = tt; + + v[1].u = tr; + v[1].v = tt; + + v[2].u = tr; + v[2].v = tb; + + v[3].u = tl; + v[3].v = tb; +#else + // GL / GLES uses triangle strips, not quads, so have to rearrange the vertex order + v[0].u = tl; + v[0].v = tt; + v[0].x = x[0]; + v[0].y = y[0]; + v[0].z = z[0]; + + v[1].u = tl; + v[1].v = tb; + v[1].x = x[3]; + v[1].y = y[3]; + v[1].z = z[3]; + + v[2].u = tr; + v[2].v = tt; + v[2].x = x[1]; + v[2].y = y[1]; + v[2].z = z[1]; + + v[3].u = tr; + v[3].v = tb; + v[3].x = x[2]; + v[3].y = y[2]; + v[3].z = z[2]; +#endif +} + +// Oblique code - original taken from freetype2 (ftsynth.c) +void CGUIFontTTF::ObliqueGlyph(FT_GlyphSlot slot) +{ + /* only oblique outline glyphs */ + if (slot->format != FT_GLYPH_FORMAT_OUTLINE) + return; + + /* we don't touch the advance width */ + + /* For italic, simply apply a shear transform, with an angle */ + /* of about 12 degrees. */ + + FT_Matrix transform; + transform.xx = 0x10000L; + transform.yx = 0x00000L; + + transform.xy = 0x06000L; + transform.yy = 0x10000L; + + FT_Outline_Transform(&slot->outline, &transform); +} + +// Embolden code - original taken from freetype2 (ftsynth.c) +void CGUIFontTTF::SetGlyphStrength(FT_GlyphSlot slot, int glyphStrength) +{ + if (slot->format != FT_GLYPH_FORMAT_OUTLINE) + return; + + /* some reasonable strength */ + FT_Pos strength = FT_MulFix(m_face->units_per_EM, m_face->size->metrics.y_scale) / glyphStrength; + + FT_BBox bbox_before, bbox_after; + FT_Outline_Get_CBox(&slot->outline, &bbox_before); + FT_Outline_Embolden(&slot->outline, strength); // ignore error + FT_Outline_Get_CBox(&slot->outline, &bbox_after); + + FT_Pos dx = bbox_after.xMax - bbox_before.xMax; + FT_Pos dy = bbox_after.yMax - bbox_before.yMax; + + if (slot->advance.x) + slot->advance.x += dx; + + if (slot->advance.y) + slot->advance.y += dy; + + slot->metrics.width += dx; + slot->metrics.height += dy; + slot->metrics.horiBearingY += dy; + slot->metrics.horiAdvance += dx; + slot->metrics.vertBearingX -= dx / 2; + slot->metrics.vertBearingY += dy; + slot->metrics.vertAdvance += dy; +} + +float CGUIFontTTF::GetTabSpaceLength() +{ + const Character* c = GetCharacter(static_cast<character_t>('X'), 0); + return c ? c->m_advance * TAB_SPACE_LENGTH : 28.0f * TAB_SPACE_LENGTH; +} |