Browse Source

Proper support for usage of multiple fonts in a chain

Ivan Savenko 1 year ago
parent
commit
87274128e7

+ 2 - 0
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

+ 21 - 1
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<data.size(); i += TextOperations::getUnicodeCharacterSize(data[i]))
 	{
-		width += getGlyphWidth(data.data() + i);
+		width += getGlyphWidthScaled(data.data() + i);
 	}
 	return width;
 }

+ 19 - 5
client/render/IFont.h

@@ -19,8 +19,6 @@ struct SDL_Surface;
 class IFont : boost::noncopyable
 {
 protected:
-	/// Internal function to render font, see renderTextLeft
-	virtual void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const = 0;
 
 	int getScalingFactor() const;
 
@@ -29,11 +27,27 @@ public:
 	{}
 
 	/// Returns height of font
-	virtual size_t getLineHeight() const = 0;
+	virtual size_t getLineHeightScaled() const = 0;
+	/// Returns width, in pixels of a character glyph. Pointer must contain at least characterSize valid bytes
+	virtual size_t getGlyphWidthScaled(const char * data) const = 0;
+	/// Return width of the string
+	virtual size_t getStringWidthScaled(const std::string & data) const;
+	/// Returns distance from top of the font glyphs to baseline
+	virtual size_t getFontAscentScaled() const = 0;
+
+	/// Returns height of font
+	size_t getLineHeight() const;
 	/// Returns width, in pixels of a character glyph. Pointer must contain at least characterSize valid bytes
-	virtual size_t getGlyphWidth(const char * data) const = 0;
+	size_t getGlyphWidth(const char * data) const;
 	/// Return width of the string
-	virtual size_t getStringWidth(const std::string & data) const;
+	size_t getStringWidth(const std::string & data) const;
+	/// Returns distance from top of the font glyphs to baseline
+	size_t getFontAscent() const;
+
+	/// Internal function to render font, see renderTextLeft
+	virtual void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const = 0;
+
+	virtual bool canRepresentCharacter(const char * data) const	= 0;
 
 	/**
 	 * @param surface - destination to print text on

+ 30 - 28
client/renderSDL/CBitmapFont.cpp

@@ -96,21 +96,14 @@ static AtlasLayout doAtlasPacking(const std::map<int, Point> & images)
 	}
 }
 
-void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & resource, std::unordered_map<CodePoint, EntryFNT> & loadedChars)
+void CBitmapFont::loadFont(const ResourcePath & resource, std::unordered_map<CodePoint, EntryFNT> & 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<CodePoint, EntryFNT> 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<int, Point> 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

+ 7 - 5
client/renderSDL/CBitmapFont.h

@@ -40,9 +40,10 @@ class CBitmapFont final : public IFont
 	};
 
 	std::unordered_map<CodePoint, BitmapChar> chars;
-	uint32_t maxHeight;
+	uint32_t height;
+	uint32_t ascent;
 
-	void loadModFont(const std::string & modName, const ResourcePath & resource, std::unordered_map<CodePoint, EntryFNT> & loadedChars);
+	void loadFont(const ResourcePath & resource, std::unordered_map<CodePoint, EntryFNT> & 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;

+ 18 - 25
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<std::unique_ptr<ui8[]>, 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<CBitmapFont>(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 TTF_FontHeight(font.get());
+}
 
-	return getStringWidth(std::string(data, TextOperations::getUnicodeCharacterSize(*data)));
+size_t CTrueTypeFont::getGlyphWidthScaled(const char *data) const
+{
+	return getStringWidthScaled(std::string(data, TextOperations::getUnicodeCharacterSize(*data)));
 }
 
-size_t CTrueTypeFont::getStringWidth(const std::string & data) const
+bool CTrueTypeFont::canRepresentCharacter(const char * data) const
 {
-	if (fallbackFont && fallbackFont->canRepresentString(data))
-		return fallbackFont->getStringWidth(data);
+	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)

+ 6 - 4
client/renderSDL/CTrueTypeFont.h

@@ -21,7 +21,6 @@ using TTF_Font = struct _TTF_Font;
 
 class CTrueTypeFont final : public IFont
 {
-	std::unique_ptr<CBitmapFont> fallbackFont;
 	const std::pair<std::unique_ptr<ui8[]>, ui64> data;
 
 	const std::unique_ptr<TTF_Font, void (*)(TTF_Font*)> 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;
 };

+ 117 - 0
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<CTrueTypeFont>(trueTypeConfig));
+}
+
+void FontChain::addBitmapFont(const std::string & bitmapFilename)
+{
+	if (settings["video"]["scalableFonts"].Bool())
+		chain.push_back(std::make_unique<CBitmapFont>(bitmapFilename));
+	else
+		chain.insert(chain.begin(), std::make_unique<CBitmapFont>(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::TextChunk> FontChain::splitTextToChunks(const std::string & data) const
+{
+	std::vector<TextChunk> 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;
+}

+ 42 - 0
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<TextChunk> splitTextToChunks(const std::string & data) const;
+
+	std::vector<std::unique_ptr<IFont>> 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;
+};

+ 16 - 14
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<const IFont> RenderHandler::loadFont(EFonts font)
 		return fonts.at(font);
 
 	const int8_t index = static_cast<int8_t>(font);
-	const JsonNode config(JsonPath::builtin("config/fonts.json"));
-	const JsonVector & bmpConf = config["bitmap"].Vector();
-	const JsonNode   & ttfConf = config["trueType"];
-
-	std::string filename = bmpConf[index].String();
+	auto configList = CResourceHandler::get()->getResourcesWithName(JsonPath::builtin("config/fonts.json"));
+	std::shared_ptr<FontChain> loadedFont = std::make_shared<FontChain>();
+	std::string bitmapPath;
 
-	std::shared_ptr<const IFont> loadedFont;
+	for(auto & loader : configList)
+	{
+		auto stream = loader->load(JsonPath::builtin("config/fonts.json"));
+		std::unique_ptr<ui8[]> textData(new ui8[stream->getSize()]);
+		stream->read(textData.get(), stream->getSize());
+		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), "config/fonts.json");
+		const JsonVector & bmpConf = config["bitmap"].Vector();
+		const JsonNode   & ttfConf = config["trueType"];
 
-	if(ttfConf[filename].isNull() || settings["video"]["scalableFonts"].Bool() == false)
-		loadedFont = std::make_shared<CBitmapFont>(filename);
-	else
-		loadedFont = std::make_shared<CTrueTypeFont>(ttfConf[filename]);
+		bitmapPath = bmpConf[index].String();
+		loadedFont->addTrueTypeFont(ttfConf[bitmapPath]);
+	}
+	loadedFont->addBitmapFont(bitmapPath);
 
 	fonts[font] = loadedFont;
 	return loadedFont;

+ 5 - 0
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" ],