diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index e5aa680c8..991aba090 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -98,6 +98,7 @@ set(vcmiclientcommon_SRCS renderSDL/CTrueTypeFont.cpp renderSDL/CursorHardware.cpp renderSDL/CursorSoftware.cpp + renderSDL/FontChain.cpp renderSDL/ImageScaled.cpp renderSDL/RenderHandler.cpp renderSDL/SDLImage.cpp @@ -305,6 +306,7 @@ set(vcmiclientcommon_HEADERS renderSDL/CTrueTypeFont.h renderSDL/CursorHardware.h renderSDL/CursorSoftware.h + renderSDL/FontChain.h renderSDL/ImageScaled.h renderSDL/RenderHandler.h renderSDL/SDLImage.h diff --git a/client/render/IFont.cpp b/client/render/IFont.cpp index 3567a4da9..5e773de04 100644 --- a/client/render/IFont.cpp +++ b/client/render/IFont.cpp @@ -23,13 +23,33 @@ int IFont::getScalingFactor() const return GH.screenHandler().getScalingFactor(); } +size_t IFont::getLineHeight() const +{ + return getLineHeightScaled() / getScalingFactor(); +} + +size_t IFont::getGlyphWidth(const char * data) const +{ + return getGlyphWidthScaled(data) / getScalingFactor(); +} + size_t IFont::getStringWidth(const std::string & data) const +{ + return getStringWidthScaled(data) / getScalingFactor(); +} + +size_t IFont::getFontAscent() const +{ + return getFontAscentScaled() / getScalingFactor(); +} + +size_t IFont::getStringWidthScaled(const std::string & data) const { size_t width = 0; for(size_t i=0; i & images) } } -void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & resource, std::unordered_map & loadedChars) +void CBitmapFont::loadFont(const ResourcePath & resource, std::unordered_map & loadedChars) { - if (!CResourceHandler::get(modName)->existsResource(resource)) - { - logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName); - return; - } - - auto data = CResourceHandler::get(modName)->load(resource)->readAll(); - std::string modLanguage = CGI->modh->getModLanguage(modName); + auto data = CResourceHandler::get()->load(resource)->readAll(); + std::string modName = VLC->modh->findResourceOrigin(resource); + std::string modLanguage = VLC->modh->getModLanguage(modName); std::string modEncoding = Languages::getLanguageOptions(modLanguage).encoding; - uint32_t dataHeight = data.first[5]; - - maxHeight = std::max(maxHeight, dataHeight); + height = data.first[5]; constexpr size_t symbolsInFile = 0x100; constexpr size_t baseIndex = 32; @@ -126,10 +119,10 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & symbol.leftOffset = read_le_u32(data.first.get() + baseIndex + charIndex * 12 + 0); symbol.width = read_le_u32(data.first.get() + baseIndex + charIndex * 12 + 4); symbol.rightOffset = read_le_u32(data.first.get() + baseIndex + charIndex * 12 + 8); - symbol.height = dataHeight; + symbol.height = height; uint32_t pixelDataOffset = read_le_u32(data.first.get() + offsetIndex + charIndex * 4); - uint32_t pixelsCount = dataHeight * symbol.width; + uint32_t pixelsCount = height * symbol.width; symbol.pixels.resize(pixelsCount); @@ -139,21 +132,27 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & loadedChars[codepoint] = symbol; } + + // Try to use symbol 'L' to detect font 'ascent' - number of pixels above text baseline + const auto & symbolL = loadedChars['L']; + uint32_t lastNonEmptyRow = 0; + for (uint32_t row = 0; row < symbolL.height; ++row) + { + for (uint32_t col = 0; col < symbolL.width; ++col) + if (symbolL.pixels.at(row * symbolL.width + col) == 255) + lastNonEmptyRow = std::max(lastNonEmptyRow, row); + } + + ascent = lastNonEmptyRow + 1; } CBitmapFont::CBitmapFont(const std::string & filename): - maxHeight(0) + height(0) { ResourcePath resource("data/" + filename, EResType::BMP_FONT); std::unordered_map loadedChars; - loadModFont("core", resource, loadedChars); - - for(const auto & modName : VLC->modh->getActiveMods()) - { - if (CResourceHandler::get(modName)->existsResource(resource)) - loadModFont(modName, resource, loadedChars); - } + loadFont(resource, loadedChars); std::map atlasSymbol; for (auto const & symbol : loadedChars) @@ -213,8 +212,6 @@ CBitmapFont::CBitmapFont(const std::string & filename): SDL_FreeSurface(atlasImage); atlasImage = scaledSurface; } - - IMG_SavePNG(atlasImage, ("/home/ivan/font_" + filename).c_str()); } CBitmapFont::~CBitmapFont() @@ -222,12 +219,12 @@ CBitmapFont::~CBitmapFont() SDL_FreeSurface(atlasImage); } -size_t CBitmapFont::getLineHeight() const +size_t CBitmapFont::getLineHeightScaled() const { - return maxHeight; + return height * getScalingFactor(); } -size_t CBitmapFont::getGlyphWidth(const char * data) const +size_t CBitmapFont::getGlyphWidthScaled(const char * data) const { CodePoint localChar = TextOperations::getUnicodeCodepoint(data, 4); @@ -236,7 +233,12 @@ size_t CBitmapFont::getGlyphWidth(const char * data) const if (iter == chars.end()) return 0; - return iter->second.leftOffset + iter->second.positionInAtlas.w + iter->second.rightOffset; + return (iter->second.leftOffset + iter->second.positionInAtlas.w + iter->second.rightOffset) * getScalingFactor(); +} + +size_t CBitmapFont::getFontAscentScaled() const +{ + return ascent * getScalingFactor(); } bool CBitmapFont::canRepresentCharacter(const char *data) const diff --git a/client/renderSDL/CBitmapFont.h b/client/renderSDL/CBitmapFont.h index fc5a8253a..78bb6aad2 100644 --- a/client/renderSDL/CBitmapFont.h +++ b/client/renderSDL/CBitmapFont.h @@ -40,9 +40,10 @@ class CBitmapFont final : public IFont }; std::unordered_map chars; - uint32_t maxHeight; + uint32_t height; + uint32_t ascent; - void loadModFont(const std::string & modName, const ResourcePath & resource, std::unordered_map & loadedChars); + void loadFont(const ResourcePath & resource, std::unordered_map & loadedChars); void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const ColorRGBA & color, int &posX, int &posY) const; void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; @@ -50,11 +51,12 @@ public: explicit CBitmapFont(const std::string & filename); ~CBitmapFont(); - size_t getLineHeight() const override; - size_t getGlyphWidth(const char * data) const override; + size_t getFontAscentScaled() const override; + size_t getLineHeightScaled() const override; + size_t getGlyphWidthScaled(const char * data) const override; /// returns true if this font contains provided utf-8 character - bool canRepresentCharacter(const char * data) const; + bool canRepresentCharacter(const char * data) const override; bool canRepresentString(const std::string & data) const; friend class CBitmapHanFont; diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index 7a869a1b0..94eeea688 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -15,6 +15,7 @@ #include "../render/Colors.h" #include "../renderSDL/SDL_Extensions.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/json/JsonNode.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/texts/TextOperations.h" @@ -29,12 +30,13 @@ std::pair, ui64> CTrueTypeFont::loadData(const JsonNode & int CTrueTypeFont::getPointSize(const JsonNode & config) const { + float fontScale = settings["video"]["fontScalingFactor"].Float(); int scalingFactor = getScalingFactor(); if (config.isNumber()) - return config.Integer() * scalingFactor; + return std::round(config.Integer() * scalingFactor * fontScale); else - return config[scalingFactor-1].Integer(); + return std::round(config[scalingFactor-1].Integer() * fontScale); } TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config) @@ -71,48 +73,39 @@ CTrueTypeFont::CTrueTypeFont(const JsonNode & fontConfig): TTF_SetFontStyle(font.get(), getFontStyle(fontConfig)); TTF_SetFontHinting(font.get(),TTF_HINTING_MONO); - std::string fallbackName = fontConfig["fallback"].String(); - - if (!fallbackName.empty()) - fallbackFont = std::make_unique(fallbackName); } CTrueTypeFont::~CTrueTypeFont() = default; -size_t CTrueTypeFont::getLineHeight() const +size_t CTrueTypeFont::getFontAscentScaled() const { - if (fallbackFont) - return fallbackFont->getLineHeight(); - - return TTF_FontHeight(font.get()) / getScalingFactor(); + return TTF_FontAscent(font.get()); } -size_t CTrueTypeFont::getGlyphWidth(const char *data) const +size_t CTrueTypeFont::getLineHeightScaled() const { - if (fallbackFont && fallbackFont->canRepresentCharacter(data)) - return fallbackFont->getGlyphWidth(data); - - return getStringWidth(std::string(data, TextOperations::getUnicodeCharacterSize(*data))); + return TTF_FontHeight(font.get()); } -size_t CTrueTypeFont::getStringWidth(const std::string & data) const +size_t CTrueTypeFont::getGlyphWidthScaled(const char *data) const { - if (fallbackFont && fallbackFont->canRepresentString(data)) - return fallbackFont->getStringWidth(data); + return getStringWidthScaled(std::string(data, TextOperations::getUnicodeCharacterSize(*data))); +} +bool CTrueTypeFont::canRepresentCharacter(const char * data) const +{ + return TTF_GlyphIsProvided32(font.get(), TextOperations::getUnicodeCodepoint(data, TextOperations::getUnicodeCharacterSize(*data))); +} + +size_t CTrueTypeFont::getStringWidthScaled(const std::string & data) const +{ int width; TTF_SizeUTF8(font.get(), data.c_str(), &width, nullptr); - return width / getScalingFactor(); + return width; } void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { - if (fallbackFont && fallbackFont->canRepresentString(data)) - { - fallbackFont->renderText(surface, data, color, pos); - return; - } - if (color.r != 0 && color.g != 0 && color.b != 0) // not black - add shadow { if (outline) diff --git a/client/renderSDL/CTrueTypeFont.h b/client/renderSDL/CTrueTypeFont.h index fc6010c2f..87a9ac484 100644 --- a/client/renderSDL/CTrueTypeFont.h +++ b/client/renderSDL/CTrueTypeFont.h @@ -21,7 +21,6 @@ using TTF_Font = struct _TTF_Font; class CTrueTypeFont final : public IFont { - std::unique_ptr fallbackFont; const std::pair, ui64> data; const std::unique_ptr font; @@ -35,11 +34,14 @@ class CTrueTypeFont final : public IFont int getFontStyle(const JsonNode & config) const; void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; + size_t getFontAscentScaled() const override; public: CTrueTypeFont(const JsonNode & fontConfig); ~CTrueTypeFont(); - size_t getLineHeight() const override; - size_t getGlyphWidth(const char * data) const override; - size_t getStringWidth(const std::string & data) const override; + bool canRepresentCharacter(const char * data) const override; + + size_t getLineHeightScaled() const override; + size_t getGlyphWidthScaled(const char * data) const override; + size_t getStringWidthScaled(const std::string & data) const override; }; diff --git a/client/renderSDL/FontChain.cpp b/client/renderSDL/FontChain.cpp new file mode 100644 index 000000000..df139e766 --- /dev/null +++ b/client/renderSDL/FontChain.cpp @@ -0,0 +1,117 @@ +/* + * FontChain.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "FontChain.h" + +#include "CTrueTypeFont.h" +#include "CBitmapFont.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/texts/TextOperations.h" + +void FontChain::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const +{ + auto chunks = splitTextToChunks(data); + int maxAscent = getFontAscentScaled(); + Point currentPos = pos; + for (auto const & chunk : chunks) + { + Point chunkPos = currentPos; + int currAscent = chunk.font->getFontAscentScaled(); + chunkPos.y += maxAscent - currAscent; + chunk.font->renderText(surface, chunk.text, color, chunkPos); + currentPos.x += chunk.font->getStringWidthScaled(chunk.text); + } +} + +size_t FontChain::getFontAscentScaled() const +{ + size_t maxHeight = 0; + for(const auto & font : chain) + maxHeight = std::max(maxHeight, font->getFontAscentScaled()); + return maxHeight; +} + +void FontChain::addTrueTypeFont(const JsonNode & trueTypeConfig) +{ + chain.push_back(std::make_unique(trueTypeConfig)); +} + +void FontChain::addBitmapFont(const std::string & bitmapFilename) +{ + if (settings["video"]["scalableFonts"].Bool()) + chain.push_back(std::make_unique(bitmapFilename)); + else + chain.insert(chain.begin(), std::make_unique(bitmapFilename)); +} + +bool FontChain::canRepresentCharacter(const char * data) const +{ + for(const auto & font : chain) + if (font->canRepresentCharacter(data)) + return true; + return false; +} + +size_t FontChain::getLineHeightScaled() const +{ + size_t maxHeight = 0; + for(const auto & font : chain) + maxHeight = std::max(maxHeight, font->getLineHeightScaled()); + return maxHeight; +} + +size_t FontChain::getGlyphWidthScaled(const char * data) const +{ + for(const auto & font : chain) + if (font->canRepresentCharacter(data)) + return font->getGlyphWidthScaled(data); + return 0; +} + +std::vector FontChain::splitTextToChunks(const std::string & data) const +{ + std::vector chunks; + + for (size_t i = 0; i < data.size(); i += TextOperations::getUnicodeCharacterSize(data[i])) + { + const IFont * currentFont = nullptr; + for(const auto & font : chain) + { + if (font->canRepresentCharacter(data.data() + i)) + { + currentFont = font.get(); + break; + } + } + + if (currentFont == nullptr) + continue; // not representable + + std::string symbol = data.substr(i, TextOperations::getUnicodeCharacterSize(data[i])); + + if (chunks.empty() || chunks.back().font != currentFont) + chunks.push_back({currentFont, symbol}); + else + chunks.back().text += symbol; + } + + return chunks; +} + +size_t FontChain::getStringWidthScaled(const std::string & data) const +{ + size_t result = 0; + auto chunks = splitTextToChunks(data); + for (auto const & chunk : chunks) + result += chunk.font->getStringWidthScaled(chunk.text); + + return result; +} diff --git a/client/renderSDL/FontChain.h b/client/renderSDL/FontChain.h new file mode 100644 index 000000000..9f7ba7b03 --- /dev/null +++ b/client/renderSDL/FontChain.h @@ -0,0 +1,42 @@ +/* + * FontChain.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../render/IFont.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class FontChain final : public IFont +{ + struct TextChunk + { + const IFont * font; + std::string text; + }; + + std::vector splitTextToChunks(const std::string & data) const; + + std::vector> chain; + + void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; + size_t getFontAscentScaled() const override; +public: + FontChain() = default; + + void addTrueTypeFont(const JsonNode & trueTypeConfig); + void addBitmapFont(const std::string & bitmapFilename); + + size_t getLineHeightScaled() const override; + size_t getGlyphWidthScaled(const char * data) const override; + size_t getStringWidthScaled(const std::string & data) const override; + bool canRepresentCharacter(const char * data) const override; +}; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 1dd57cab7..afc45773d 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -12,8 +12,7 @@ #include "SDLImage.h" #include "ImageScaled.h" -#include "CBitmapFont.h" -#include "CTrueTypeFont.h" +#include "FontChain.h" #include "../gui/CGuiHandler.h" @@ -22,8 +21,6 @@ #include "../render/Colors.h" #include "../render/ColorFilter.h" #include "../render/IScreenHandler.h" - -#include "../../lib/CConfigHandler.h" #include "../../lib/json/JsonUtils.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/VCMIDirs.h" @@ -345,18 +342,23 @@ std::shared_ptr RenderHandler::loadFont(EFonts font) return fonts.at(font); const int8_t index = static_cast(font); - const JsonNode config(JsonPath::builtin("config/fonts.json")); - const JsonVector & bmpConf = config["bitmap"].Vector(); - const JsonNode & ttfConf = config["trueType"]; + auto configList = CResourceHandler::get()->getResourcesWithName(JsonPath::builtin("config/fonts.json")); + std::shared_ptr loadedFont = std::make_shared(); + std::string bitmapPath; - std::string filename = bmpConf[index].String(); + for(auto & loader : configList) + { + auto stream = loader->load(JsonPath::builtin("config/fonts.json")); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize(), "config/fonts.json"); + const JsonVector & bmpConf = config["bitmap"].Vector(); + const JsonNode & ttfConf = config["trueType"]; - std::shared_ptr loadedFont; - - if(ttfConf[filename].isNull() || settings["video"]["scalableFonts"].Bool() == false) - loadedFont = std::make_shared(filename); - else - loadedFont = std::make_shared(ttfConf[filename]); + bitmapPath = bmpConf[index].String(); + loadedFont->addTrueTypeFont(ttfConf[bitmapPath]); + } + loadedFont->addBitmapFont(bitmapPath); fonts[font] = loadedFont; return loadedFont; diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 843558a79..f7cde9e7e 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -167,6 +167,7 @@ "targetfps", "vsync", "scalableFonts", + "fontScalingFactor", "upscalingFilter", "fontUpscalingFilter", "downscalingFilter" @@ -237,6 +238,10 @@ "type" : "boolean", "default" : false }, + "fontScalingFactor" : { + "type" : "number", + "default" : 1 + }, "fontUpscalingFilter" : { "type" : "string", "enum" : [ "nearest", "bilinear", "xbrz" ],