Initial (WIP) Harfbuzz text shaping integration.

This a significant refactor / rewrite of much of the love.graphics Font code.
Text positioning is now split into TextShaper classes in the love.font module. This isn't exposed to users yet.
BMFonts and ImageFonts use the same text shaping as before (in the new TextShaper API). Fonts loaded via FreeType now use Harfbuzz for text shaping.

The new code isn't at feature-parity with the old code yet. Harfbuzz-based text shaping enables kerning support in more fonts, character combining into ligature glyphs, and directions other than left-to-right (this isn't supported in love yet).
This commit is contained in:
Sasha Szpakowski
2022-12-26 19:06:08 -04:00
parent 9086afb718
commit 7a0f6621e7
27 changed files with 1490 additions and 626 deletions
+373
View File
@@ -0,0 +1,373 @@
/**
* Copyright (c) 2006-2022 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}
, dpiScales{rasterizer->getDPIScale()}
, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
, lineHeight(1)
{
}
TextShaper::~TextShaper()
{
}
float TextShaper::getHeight() const
{
return height;
}
void TextShaper::setLineHeight(float h)
{
lineHeight = h;
}
float TextShaper::getLineHeight() const
{
return lineHeight;
}
int TextShaper::getAscent() const
{
return floorf(rasterizers[0]->getAscent() / rasterizers[0]->getDPIScale() + 0.5f);
}
int TextShaper::getDescent() const
{
return floorf(rasterizers[0]->getDescent() / rasterizers[0]->getDPIScale() + 0.5f);
}
float TextShaper::getBaseline() const
{
float ascent = getAscent();
if (ascent != 0.0f)
return ascent;
else if (rasterizers[0]->getDataType() == font::Rasterizer::DATA_TRUETYPE)
return floorf(getHeight() / 1.25f + 0.5f); // 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 = floorf(r->getKerning(leftglyph, rightglyph) / r->getDPIScale() + 0.5f);
break;
}
}
if (!found)
k = floorf(rasterizers[0]->getKerning(leftglyph, rightglyph) / rasterizers[0]->getDPIScale() + 0.5f);
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);
}
int TextShaper::getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex)
{
// TODO: use spaces for tab
const auto it = glyphAdvances.find(glyph);
if (it != glyphAdvances.end())
{
if (glyphindex)
*glyphindex = it->second.second;
return it->second.first;
}
int rasterizeri = 0;
for (size_t i = 0; i < rasterizers.size(); i++)
{
if (rasterizers[i]->hasGlyph(glyph))
{
rasterizeri = (int) i;
break;
}
}
const auto &r = rasterizers[rasterizeri];
int advance = floorf(r->getGlyphSpacing(glyph) / r->getDPIScale() + 0.5f);
GlyphIndex glyphi = {r->getGlyphIndex(glyph), rasterizeri};
glyphAdvances[glyph] = std::make_pair(advance, glyphi);
if (glyphindex)
*glyphindex = glyphi;
return advance;
}
int 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<int>* 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 + 1 - i);
i = (size_t)wrapindex + 1;
}
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<int>* 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.");
}
// Clear caches.
kerning.clear();
glyphAdvances.clear();
rasterizers.resize(1);
dpiScales.resize(1);
for (Rasterizer* r : fallbacks)
{
rasterizers.push_back(r);
dpiScales.push_back(r->getDPIScale());
}
}
} // font
} // love