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
+203
View File
@@ -0,0 +1,203 @@
/**
* 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 "GenericShaper.h"
#include "Rasterizer.h"
#include "common/Optional.h"
namespace love
{
namespace font
{
GenericShaper::GenericShaper(Rasterizer *rasterizer)
: TextShaper(rasterizer)
{
}
GenericShaper::~GenericShaper()
{
}
void GenericShaper::computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info)
{
if (!range.isValid())
range = Range(0, codepoints.cps.size());
if (rasterizers[0]->getDataType() == Rasterizer::DATA_TRUETYPE)
offset.y += getBaseline();
// Spacing counter and newline handling.
Vector2 curpos = offset;
int maxwidth = 0;
uint32 prevglyph = 0;
if (positions)
positions->reserve(range.getSize());
int colorindex = 0;
int ncolors = (int) codepoints.colors.size();
Optional<Colorf> colorToAdd;
// Make sure the right color is applied to the start of the glyph list,
// when the start isn't 0.
if (colors && range.getOffset() > 0 && !codepoints.colors.empty())
{
for (; colorindex < ncolors; colorindex++)
{
if (codepoints.colors[colorindex].index >= (int) range.getOffset())
break;
colorToAdd.set(codepoints.colors[colorindex].color);
}
}
for (int i = (int) range.getMin(); i <= (int) range.getMax(); i++)
{
uint32 g = codepoints.cps[i];
// Do this before anything else so we don't miss colors corresponding
// to newlines. The actual add to the list happens after newline
// handling, to make sure the resulting index is valid in the positions
// array.
if (colors && colorindex < ncolors && codepoints.colors[colorindex].index == i)
{
colorToAdd.set(codepoints.colors[colorindex].color);
colorindex++;
}
if (g == '\n')
{
if (curpos.x > maxwidth)
maxwidth = (int)curpos.x;
// Wrap newline, but do not output a position for it.
curpos.y += floorf(getHeight() * getLineHeight() + 0.5f);
curpos.x = offset.x;
prevglyph = 0;
continue;
}
// Ignore carriage returns
if (g == '\r')
{
prevglyph = g;
continue;
}
if (colorToAdd.hasValue && colors && positions)
{
IndexedColor c = {colorToAdd.value, positions->size()};
colors->push_back(c);
colorToAdd.clear();
}
// Add kerning to the current horizontal offset.
curpos.x += getKerning(prevglyph, g);
GlyphIndex glyphindex;
int advance = getGlyphAdvance(g, &glyphindex);
if (positions)
positions->push_back({ Vector2(curpos.x, curpos.y), glyphindex });
// Advance the x position for the next glyph.
curpos.x += advance;
// Account for extra spacing given to space characters.
if (g == ' ' && extraspacing != 0.0f)
curpos.x = floorf(curpos.x + extraspacing);
prevglyph = g;
}
if (curpos.x > maxwidth)
maxwidth = (int)curpos.x;
if (info != nullptr)
{
info->width = maxwidth - offset.x;
info->height = curpos.y - offset.y;
if (curpos.x > offset.x)
info->height += floorf(getHeight() * getLineHeight() + 0.5f);
}
}
int GenericShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width)
{
if (!range.isValid())
range = Range(0, codepoints.cps.size());
uint32 prevglyph = 0;
float w = 0.0f;
float outwidth = 0.0f;
float widthbeforelastspace = 0.0f;
int wrapindex = -1;
int lastspaceindex = -1;
for (int i = (int)range.getMin(); i <= (int)range.getMax(); i++)
{
uint32 g = codepoints.cps[i];
if (g == '\r')
{
prevglyph = g;
continue;
}
float newwidth = w + getKerning(prevglyph, g) + getGlyphAdvance(g);
// Only wrap when there's a non-space character.
if (newwidth > wraplimit && g != ' ')
{
// Rewind to the last seen space when wrapping.
if (lastspaceindex != -1)
{
wrapindex = lastspaceindex;
outwidth = widthbeforelastspace;
}
break;
}
// Don't count trailing spaces in the output width.
if (g == ' ')
{
lastspaceindex = i;
if (prevglyph != ' ')
widthbeforelastspace = w;
}
else
outwidth = newwidth;
w = newwidth;
prevglyph = g;
wrapindex = i;
}
if (width)
*width = outwidth;
return wrapindex;
}
} // font
} // love