Download
391 lines 8.8 KiBC++Modified 2026-06-21 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
/**
 * Copyright (c) 2006-2026 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

// LOVE
#include "TextShaper.h"
#include "Rasterizer.h"
#include "common/Exception.h"

#include "libraries/utf8/utf8.h"

namespace love
{
namespace font
{

void getCodepointsFromString(const std::string &text, std::vector<uint32> &codepoints)
{
	codepoints.reserve(text.size());

	try
	{
		utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());

		while (i != end)
		{
			uint32 g = *i++;
			codepoints.push_back(g);
		}
	}
	catch (utf8::exception &e)
	{
		throw love::Exception("UTF-8 decoding error: %s", e.what());
	}
}

void getCodepointsFromString(const std::vector<ColoredString> &strs, ColoredCodepoints &codepoints)
{
	if (strs.empty())
		return;

	codepoints.cps.reserve(strs[0].str.size());

	for (const ColoredString &cstr : strs)
	{
		// No need to add the color if the string is empty anyway, and the code
		// further on assumes no two colors share the same starting position.
		if (cstr.str.size() == 0)
			continue;

		IndexedColor c = { cstr.color, (int)codepoints.cps.size() };
		codepoints.colors.push_back(c);

		getCodepointsFromString(cstr.str, codepoints.cps);
	}

	if (codepoints.colors.size() == 1)
	{
		IndexedColor c = codepoints.colors[0];

		if (c.index == 0 && c.color == Colorf(1.0f, 1.0f, 1.0f, 1.0f))
			codepoints.colors.pop_back();
	}
}

love::Type TextShaper::type("TextShaper", &Object::type);

TextShaper::TextShaper(Rasterizer *rasterizer)
	: rasterizers{rasterizer}
	, dpiScale(rasterizer->getDPIScale())
	, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
	, pixelHeight(rasterizer->getHeight())
	, lineHeight(1)
	, useSpacesForTab(false)
{
	if (!rasterizer->hasGlyph('\t'))
		useSpacesForTab = true;
}

TextShaper::~TextShaper()
{
}

float TextShaper::getHeight() const
{
	return height;
}

float TextShaper::getPixelHeight() const
{
	return pixelHeight;
}

float TextShaper::getCombinedHeight() const
{
	return floorf(pixelHeight * lineHeight + 0.5f) / rasterizers[0]->getDPIScale();
}

void TextShaper::setLineHeight(float h)
{
	lineHeight = h;
}

float TextShaper::getLineHeight() const
{
	return lineHeight;
}

float TextShaper::getAscent() const
{
	return rasterizers[0]->getAscent() / rasterizers[0]->getDPIScale();
}

float TextShaper::getDescent() const
{
	return rasterizers[0]->getDescent() / rasterizers[0]->getDPIScale();
}

float TextShaper::getBaseline() const
{
	float ascent = getAscent();
	if (ascent != 0.0f)
		return ascent;
	else if (rasterizers[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
		return floorf(getPixelHeight() / 1.25f + 0.5f) / rasterizers[0]->getDPIScale(); // 1.25 is magic line height for true type fonts
	else
		return 0.0f;
}

bool TextShaper::hasGlyph(uint32 glyph) const
{
	for (const StrongRef<Rasterizer> &r : rasterizers)
	{
		if (r->hasGlyph(glyph))
			return true;
	}

	return false;
}

bool TextShaper::hasGlyphs(const std::string &text) const
{
	if (text.size() == 0)
		return false;

	try
	{
		utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());

		while (i != end)
		{
			uint32 codepoint = *i++;

			if (!hasGlyph(codepoint))
				return false;
		}
	}
	catch (utf8::exception &e)
	{
		throw love::Exception("UTF-8 decoding error: %s", e.what());
	}

	return true;
}

float TextShaper::getKerning(uint32 leftglyph, uint32 rightglyph)
{
	uint64 packedglyphs = ((uint64)leftglyph << 32) | (uint64)rightglyph;

	const auto it = kerning.find(packedglyphs);
	if (it != kerning.end())
		return it->second;

	float k = 0.0f;
	bool found = false;

	for (const auto &r : rasterizers)
	{
		if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph))
		{
			found = true;
			k = r->getKerning(leftglyph, rightglyph) / r->getDPIScale();
			break;
		}
	}

	if (!found)
		k = rasterizers[0]->getKerning(leftglyph, rightglyph) / rasterizers[0]->getDPIScale();

	kerning[packedglyphs] = k;
	return k;
}

float TextShaper::getKerning(const std::string &leftchar, const std::string &rightchar)
{
	uint32 left = 0;
	uint32 right = 0;

	try
	{
		left = utf8::peek_next(leftchar.begin(), leftchar.end());
		right = utf8::peek_next(rightchar.begin(), rightchar.end());
	}
	catch (utf8::exception &e)
	{
		throw love::Exception("UTF-8 decoding error: %s", e.what());
	}

	return getKerning(left, right);
}

float TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
{
	const auto it = glyphAdvances.find(glyph);
	if (it != glyphAdvances.end())
	{
		if (glyphindex)
			*glyphindex = it->second.second;
		return it->second.first;
	}

	int rasterizeri = 0;
	uint32 realglyph = glyph;

	if (glyph == '\t' && isUsingSpacesForTab())
		realglyph = ' ';

	for (size_t i = 0; i < rasterizers.size(); i++)
	{
		if (rasterizers[i]->hasGlyph(realglyph))
		{
			rasterizeri = (int) i;
			break;
		}
	}

	const auto &r = rasterizers[rasterizeri];
	float advance = r->getGlyphSpacing(realglyph) / r->getDPIScale();

	if (glyph == '\t' && realglyph == ' ')
		advance *= SPACES_PER_TAB;

	GlyphIndex glyphi = {r->getGlyphIndex(realglyph), rasterizeri};

	glyphAdvances[glyph] = std::make_pair(advance, glyphi);
	if (glyphindex)
		*glyphindex = glyphi;
	return advance;
}

float TextShaper::getWidth(const std::string& str)
{
	if (str.size() == 0) return 0;

	ColoredCodepoints codepoints;
	getCodepointsFromString(str, codepoints.cps);

	TextInfo info;
	computeGlyphPositions(codepoints, Range(), Vector2(0.0f, 0.0f), 0.0f, nullptr, nullptr, &info);

	return info.width;
}

static size_t findNewline(const ColoredCodepoints &codepoints, size_t start)
{
	for (size_t i = start; i < codepoints.cps.size(); i++)
	{
		if (codepoints.cps[i] == '\n')
		{
			return i;
		}
	}

	return codepoints.cps.size();
}

void TextShaper::getWrap(const ColoredCodepoints &codepoints, float wraplimit, std::vector<Range> &lineranges, std::vector<float> *linewidths)
{
	size_t nextnewline = findNewline(codepoints, 0);

	for (size_t i = 0; i < codepoints.cps.size();)
	{
		if (nextnewline < i)
			nextnewline = findNewline(codepoints, i);

		if (nextnewline == i) // Empty line.
		{
			lineranges.push_back(Range());
			if (linewidths)
				linewidths->push_back(0);
			i++;
		}
		else
		{
			Range r(i, nextnewline - i);
			float width = 0.0f;
			int wrapindex = computeWordWrapIndex(codepoints, r, wraplimit, &width);

			if (wrapindex > (int) i)
			{
				r = Range(i, (size_t) wrapindex - i);
				i = (size_t)wrapindex;
			}
			else
			{
				r = Range();
				i++;
			}

			// We've already handled this line, skip the newline character.
			if (nextnewline == i)
				i++;

			lineranges.push_back(r);
			if (linewidths)
				linewidths->push_back(width);
		}
	}
}

void TextShaper::getWrap(const std::vector<ColoredString> &text, float wraplimit, std::vector<std::string> &lines, std::vector<float> *linewidths)
{
	ColoredCodepoints cps;
	getCodepointsFromString(text, cps);

	std::vector<Range> codepointranges;
	getWrap(cps, wraplimit, codepointranges, linewidths);

	std::string line;

	for (const auto &range : codepointranges)
	{
		line.clear();

		if (range.isValid())
		{
			line.reserve(range.getSize());

			for (size_t i = range.getMin(); i <= range.getMax(); i++)
			{
				char character[5] = { '\0' };
				char *end = utf8::unchecked::append(cps.cps[i], character);
				line.append(character, end - character);
			}
		}

		lines.push_back(line);
	}
}

void TextShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
{
	for (Rasterizer *r : fallbacks)
	{
		if (r->getDataType() != rasterizers[0]->getDataType())
			throw love::Exception("Font fallbacks must be of the same font type as the primary Font.");

		if (r->getDPIScale() != rasterizers[0]->getDPIScale())
			throw love::Exception("Font fallbacks must have the same DPI scales as the primary Font.");
	}

	// Clear caches.
	kerning.clear();
	glyphAdvances.clear();

	rasterizers.resize(1);

	for (Rasterizer *r : fallbacks)
		rasterizers.push_back(r);
}

} // font
} // love