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
+49 -15
View File
@@ -20,6 +20,7 @@
// LOVE
#include "BMFontRasterizer.h"
#include "GenericShaper.h"
#include "filesystem/Filesystem.h"
#include "image/Image.h"
@@ -164,6 +165,14 @@ BMFontRasterizer::~BMFontRasterizer()
void BMFontRasterizer::parseConfig(const std::string &configtext)
{
{
BMFontCharacter nullchar = {};
nullchar.page = -1;
nullchar.glyph = 0;
characters.push_back(nullchar);
characterIndices[0] = (int)characters.size() - 1;
}
std::stringstream ss(configtext);
std::string line;
@@ -237,7 +246,10 @@ void BMFontRasterizer::parseConfig(const std::string &configtext)
c.metrics.bearingY = -cline.getAttributeInt("yoffset");
c.metrics.advance = cline.getAttributeInt("xadvance");
characters[id] = c;
c.glyph = id;
characters.push_back(c);
characterIndices[id] = (int) characters.size() - 1;
}
else if (tag == "kerning")
{
@@ -257,13 +269,15 @@ void BMFontRasterizer::parseConfig(const std::string &configtext)
bool guessheight = lineHeight == 0;
// Verify the glyph character attributes.
for (const auto &cpair : characters)
for (const auto &c : characters)
{
const BMFontCharacter &c = cpair.second;
if (c.glyph == 0)
continue;
int width = c.metrics.width;
int height = c.metrics.height;
if (!unicode && cpair.first > 127)
if (!unicode && c.glyph > 127)
throw love::Exception("Invalid BMFont character id (only unicode and ASCII are supported)");
if (c.page < 0 || images[c.page].get() == nullptr)
@@ -272,13 +286,13 @@ void BMFontRasterizer::parseConfig(const std::string &configtext)
const image::ImageData *id = images[c.page].get();
if (!id->inside(c.x, c.y))
throw love::Exception("Invalid coordinates for BMFont character %u.", cpair.first);
throw love::Exception("Invalid coordinates for BMFont character %u.", c.glyph);
if (width > 0 && !id->inside(c.x + width - 1, c.y))
throw love::Exception("Invalid width %d for BMFont character %u.", width, cpair.first);
throw love::Exception("Invalid width %d for BMFont character %u.", width, c.glyph);
if (height > 0 && !id->inside(c.x, c.y + height - 1))
throw love::Exception("Invalid height %d for BMFont character %u.", height, cpair.first);
throw love::Exception("Invalid height %d for BMFont character %u.", height, c.glyph);
if (guessheight)
lineHeight = std::max(lineHeight, c.metrics.height);
@@ -292,22 +306,37 @@ int BMFontRasterizer::getLineHeight() const
return lineHeight;
}
GlyphData *BMFontRasterizer::getGlyphData(uint32 glyph) const
int BMFontRasterizer::getGlyphSpacing(uint32 glyph) const
{
auto it = characters.find(glyph);
auto it = characterIndices.find(glyph);
if (it == characterIndices.end())
return 0;
return characters[it->second].metrics.advance;
}
int BMFontRasterizer::getGlyphIndex(uint32 glyph) const
{
auto it = characterIndices.find(glyph);
if (it == characterIndices.end())
return 0;
return it->second;
}
GlyphData *BMFontRasterizer::getGlyphDataForIndex(int index) const
{
// Return an empty GlyphData if we don't have the glyph character.
if (it == characters.end())
return new GlyphData(glyph, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
if (index < 0 || index >= (int) characters.size())
return new GlyphData(0, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
const BMFontCharacter &c = it->second;
const BMFontCharacter& c = characters[index];
const auto &imagepair = images.find(c.page);
if (imagepair == images.end())
return new GlyphData(glyph, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
return new GlyphData(c.glyph, GlyphMetrics(), PIXELFORMAT_RGBA8_UNORM);
image::ImageData *imagedata = imagepair->second.get();
GlyphData *g = new GlyphData(glyph, c.metrics, PIXELFORMAT_RGBA8_UNORM);
GlyphData *g = new GlyphData(c.glyph, c.metrics, PIXELFORMAT_RGBA8_UNORM);
size_t pixelsize = imagedata->getPixelSize();
@@ -333,7 +362,7 @@ int BMFontRasterizer::getGlyphCount() const
bool BMFontRasterizer::hasGlyph(uint32 glyph) const
{
return characters.find(glyph) != characters.end();
return characterIndices.find(glyph) != characterIndices.end();
}
float BMFontRasterizer::getKerning(uint32 leftglyph, uint32 rightglyph) const
@@ -352,6 +381,11 @@ Rasterizer::DataType BMFontRasterizer::getDataType() const
return DATA_IMAGE;
}
TextShaper *BMFontRasterizer::newTextShaper()
{
return new GenericShaper(this);
}
bool BMFontRasterizer::accepts(love::filesystem::FileData *fontdef)
{
const char *data = (const char *) fontdef->getData();
+9 -3
View File
@@ -47,11 +47,14 @@ public:
// Implements Rasterizer.
int getLineHeight() const override;
GlyphData *getGlyphData(uint32 glyph) const override;
int getGlyphSpacing(uint32 glyph) const override;
int getGlyphIndex(uint32 glyph) const override;
GlyphData *getGlyphDataForIndex(int index) const override;
int getGlyphCount() const override;
bool hasGlyph(uint32 glyph) const override;
float getKerning(uint32 leftglyph, uint32 rightglyph) const override;
DataType getDataType() const override;
TextShaper *newTextShaper() override;
static bool accepts(love::filesystem::FileData *fontdef);
@@ -63,6 +66,7 @@ private:
int y;
int page;
GlyphMetrics metrics;
uint32 glyph;
};
void parseConfig(const std::string &config);
@@ -72,8 +76,10 @@ private:
// Image pages, indexed by their page id.
std::unordered_map<int, StrongRef<image::ImageData>> images;
// Glyph characters, indexed by their glyph id.
std::unordered_map<uint32, BMFontCharacter> characters;
std::vector<BMFontCharacter> characters;
// Glyph character indices, indexed by their glyph id.
std::unordered_map<uint32, int> characterIndices;
// Kerning information, indexed by two (packed) characters.
std::unordered_map<uint64, int> kerning;
+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
+46
View File
@@ -0,0 +1,46 @@
/**
* 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.
**/
#pragma once
// LOVE
#include "TextShaper.h"
namespace love
{
namespace font
{
class GenericShaper : public love::font::TextShaper
{
public:
GenericShaper(Rasterizer *rasterizer);
virtual ~GenericShaper();
void computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info) override;
int computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) override;
private:
}; // GenericShaper
} // font
} // love
-4
View File
@@ -24,10 +24,6 @@
// UTF-8
#include "libraries/utf8/utf8.h"
// stdlib
#include <iostream>
#include <cstddef>
namespace love
{
namespace font
+48 -15
View File
@@ -20,8 +20,9 @@
// LOVE
#include "ImageRasterizer.h"
#include "GenericShaper.h"
#include "common/Exception.h"
#include <string.h>
namespace love
@@ -31,10 +32,9 @@ namespace font
static_assert(sizeof(Color32) == 4, "sizeof(Color32) must equal 4 bytes!");
ImageRasterizer::ImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale)
ImageRasterizer::ImageRasterizer(love::image::ImageData *data, const uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale)
: imageData(data)
, glyphs(glyphs)
, numglyphs(numglyphs)
, numglyphs(numglyphs + 1) // Always have a null glyph at the start of the array.
, extraSpacing(extraspacing)
{
this->dpiScale = dpiscale;
@@ -42,7 +42,7 @@ ImageRasterizer::ImageRasterizer(love::image::ImageData *data, uint32 *glyphs, i
if (data->getFormat() != PIXELFORMAT_RGBA8_UNORM)
throw love::Exception("Only 32-bit RGBA images are supported in Image Fonts!");
load();
load(glyphs, numglyphs);
}
ImageRasterizer::~ImageRasterizer()
@@ -54,16 +54,33 @@ int ImageRasterizer::getLineHeight() const
return getHeight();
}
GlyphData *ImageRasterizer::getGlyphData(uint32 glyph) const
int ImageRasterizer::getGlyphSpacing(uint32 glyph) const
{
auto it = glyphIndices.find(glyph);
if (it == glyphIndices.end())
return 0;
return imageGlyphs[it->second].width + extraSpacing;
}
int ImageRasterizer::getGlyphIndex(uint32 glyph) const
{
auto it = glyphIndices.find(glyph);
if (it == glyphIndices.end())
return 0;
return it->second;
}
GlyphData *ImageRasterizer::getGlyphDataForIndex(int index) const
{
GlyphMetrics gm = {};
uint32 glyph = 0;
// Set relevant glyph metrics if the glyph is in this ImageFont
std::map<uint32, ImageGlyphData>::const_iterator it = imageGlyphs.find(glyph);
if (it != imageGlyphs.end())
if (index >= 0 && index < (int) imageGlyphs.size())
{
gm.width = it->second.width;
gm.advance = it->second.width + extraSpacing;
gm.width = imageGlyphs[index].width;
gm.advance = imageGlyphs[index].width + extraSpacing;
glyph = imageGlyphs[index].glyph;
}
gm.height = metrics.height;
@@ -82,7 +99,7 @@ GlyphData *ImageRasterizer::getGlyphData(uint32 glyph) const
// copy glyph pixels from imagedata to glyphdata
for (int i = 0; i < g->getWidth() * g->getHeight(); i++)
{
Color32 p = imagepixels[it->second.x + (i % gm.width) + (imageData->getWidth() * (i / gm.width))];
Color32 p = imagepixels[imageGlyphs[index].x + (i % gm.width) + (imageData->getWidth() * (i / gm.width))];
// Use transparency instead of the spacer color
if (p == spacer)
@@ -94,7 +111,7 @@ GlyphData *ImageRasterizer::getGlyphData(uint32 glyph) const
return g;
}
void ImageRasterizer::load()
void ImageRasterizer::load(const uint32 *glyphs, int glyphcount)
{
auto pixels = (const Color32 *) imageData->getData();
@@ -113,7 +130,16 @@ void ImageRasterizer::load()
int start = 0;
int end = 0;
for (int i = 0; i < numglyphs; ++i)
{
ImageGlyphData nullglyph;
nullglyph.x = 0;
nullglyph.width = 0;
nullglyph.glyph = 0;
imageGlyphs.push_back(nullglyph);
glyphIndices[0] = (int) imageGlyphs.size() - 1;
}
for (int i = 0; i < glyphcount; ++i)
{
start = end;
@@ -133,8 +159,10 @@ void ImageRasterizer::load()
ImageGlyphData imageGlyph;
imageGlyph.x = start;
imageGlyph.width = end - start;
imageGlyph.glyph = glyphs[i];
imageGlyphs[glyphs[i]] = imageGlyph;
imageGlyphs.push_back(imageGlyph);
glyphIndices[glyphs[i]] = (int) imageGlyphs.size() - 1;
}
}
@@ -145,7 +173,7 @@ int ImageRasterizer::getGlyphCount() const
bool ImageRasterizer::hasGlyph(uint32 glyph) const
{
return imageGlyphs.find(glyph) != imageGlyphs.end();
return glyphIndices.find(glyph) != glyphIndices.end();
}
Rasterizer::DataType ImageRasterizer::getDataType() const
@@ -153,5 +181,10 @@ Rasterizer::DataType ImageRasterizer::getDataType() const
return DATA_IMAGE;
}
TextShaper *ImageRasterizer::newTextShaper()
{
return new GenericShaper(this);
}
} // font
} // love
+10 -7
View File
@@ -39,15 +39,18 @@ namespace font
class ImageRasterizer : public Rasterizer
{
public:
ImageRasterizer(love::image::ImageData *imageData, uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale);
ImageRasterizer(love::image::ImageData *imageData, const uint32 *glyphs, int numglyphs, int extraspacing, float dpiscale);
virtual ~ImageRasterizer();
// Implement Rasterizer
int getLineHeight() const override;
GlyphData *getGlyphData(uint32 glyph) const override;
int getGlyphSpacing(uint32 glyph) const override;
int getGlyphIndex(uint32 glyph) const override;
GlyphData *getGlyphDataForIndex(int index) const override;
int getGlyphCount() const override;
bool hasGlyph(uint32 glyph) const override;
DataType getDataType() const override;
TextShaper *newTextShaper() override;
private:
@@ -57,23 +60,23 @@ private:
{
int x;
int width;
uint32 glyph;
};
// Load all the glyph positions into memory
void load();
void load(const uint32 *glyphs, int glyphcount);
// The image data
StrongRef<love::image::ImageData> imageData;
// The glyphs in the font
uint32 *glyphs;
// Number of glyphs in the font
int numglyphs;
int extraSpacing;
std::map<uint32, ImageGlyphData> imageGlyphs;
std::vector<ImageGlyphData> imageGlyphs;
std::map<uint32, int> glyphIndices;
// Color used to identify glyph separation in the source ImageData
Color32 spacer;
+5
View File
@@ -55,6 +55,11 @@ int Rasterizer::getDescent() const
return metrics.descent;
}
GlyphData *Rasterizer::getGlyphData(uint32 glyph) const
{
return getGlyphDataForIndex(getGlyphIndex(glyph));
}
GlyphData *Rasterizer::getGlyphData(const std::string &text) const
{
uint32 codepoint = 0;
+23 -2
View File
@@ -31,6 +31,8 @@ namespace love
namespace font
{
class TextShaper;
/**
* Holds the specific font metrics.
**/
@@ -84,17 +86,32 @@ public:
**/
virtual int getLineHeight() const = 0;
/**
* Gets the spacing of the given unicode glyph.
**/
virtual int getGlyphSpacing(uint32 glyph) const = 0;
/**
* Gets a rasterizer-specific index associated with the given glyph.
**/
virtual int getGlyphIndex(uint32 glyph) const = 0;
/**
* Gets a specific glyph.
* @param glyph The (UNICODE) glyph codepoint to get data for.
**/
virtual GlyphData *getGlyphData(uint32 glyph) const = 0;
GlyphData *getGlyphData(uint32 glyph) const;
/**
* Gets a specific glyph.
* @param text The (UNICODE) glyph character to get the data for.
**/
virtual GlyphData *getGlyphData(const std::string &text) const;
GlyphData *getGlyphData(const std::string &text) const;
/**
* Gets a specific glyph for the given rasterizer glyph index.
**/
virtual GlyphData *getGlyphDataForIndex(int index) const = 0;
/**
* Gets the number of glyphs the rasterizer has data for.
@@ -120,6 +137,10 @@ public:
virtual DataType getDataType() const = 0;
virtual ptrdiff_t getHandle() const { return 0; }
virtual TextShaper *newTextShaper() = 0;
float getDPIScale() const;
protected:
+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
+148
View File
@@ -0,0 +1,148 @@
/**
* 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.
**/
#pragma once
// LOVE
#include "common/Object.h"
#include "common/Vector.h"
#include "common/int.h"
#include "common/Color.h"
#include "common/Range.h"
#include <vector>
#include <string>
#include <unordered_map>
namespace love
{
namespace font
{
class Rasterizer;
struct ColoredString
{
std::string str;
Colorf color;
};
struct IndexedColor
{
Colorf color;
int index;
};
struct ColoredCodepoints
{
std::vector<uint32> cps;
std::vector<IndexedColor> colors;
};
void getCodepointsFromString(const std::string& str, std::vector<uint32>& codepoints);
void getCodepointsFromString(const std::vector<ColoredString>& strs, ColoredCodepoints& codepoints);
class TextShaper : public Object
{
public:
struct GlyphIndex
{
int index;
int rasterizerIndex;
};
struct GlyphPosition
{
Vector2 position;
GlyphIndex glyphIndex;
};
struct TextInfo
{
int width;
int height;
};
static love::Type type;
virtual ~TextShaper();
const std::vector<StrongRef<Rasterizer>> &getRasterizers() const { return rasterizers; }
float getHeight() const;
/**
* Sets the line height (which should be a number to multiply the font size by,
* example: line height = 1.2 and size = 12 means that rendered line height = 12*1.2)
* @param height The new line height.
**/
void setLineHeight(float height);
/**
* Returns the line height.
**/
float getLineHeight() const;
// Extra font metrics
int getAscent() const;
int getDescent() const;
float getBaseline() const;
bool hasGlyph(uint32 glyph) const;
bool hasGlyphs(const std::string &text) const;
float getKerning(uint32 leftglyph, uint32 rightglyph);
float getKerning(const std::string& leftchar, const std::string& rightchar);
int getGlyphAdvance(uint32 glyph, GlyphIndex *glyphindex = nullptr);
int getWidth(const std::string &str);
void getWrap(const std::vector<ColoredString>& text, float wraplimit, std::vector<std::string>& lines, std::vector<int>* line_widths = nullptr);
void getWrap(const ColoredCodepoints& codepoints, float wraplimit, std::vector<Range> &lineranges, std::vector<int> *linewidths = nullptr);
virtual void setFallbacks(const std::vector<Rasterizer *> &fallbacks);
virtual void computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info) = 0;
virtual int computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) = 0;
protected:
TextShaper(Rasterizer *rasterizer);
std::vector<StrongRef<Rasterizer>> rasterizers;
std::vector<float> dpiScales;
private:
int height;
float lineHeight;
// maps glyphs to advance and glyph+rasterizer index.
std::unordered_map<uint32, std::pair<int, GlyphIndex>> glyphAdvances;
// map of left/right glyph pairs to horizontal kerning.
std::unordered_map<uint64, float> kerning;
}; // TextShaper
} // font
} // love
@@ -0,0 +1,290 @@
/**
* 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 "HarfbuzzShaper.h"
#include "TrueTypeRasterizer.h"
#include "common/Optional.h"
// harfbuzz
#include <hb.h>
#include <hb-ft.h>
namespace love
{
namespace font
{
namespace freetype
{
HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
: TextShaper(rasterizer)
{
hbFonts.push_back(hb_ft_font_create_referenced((FT_Face)rasterizer->getHandle()));
if (hbFonts[0] == nullptr || hbFonts[0] == hb_font_get_empty())
throw love::Exception("");
}
HarfbuzzShaper::~HarfbuzzShaper()
{
for (hb_font_t *font : hbFonts)
hb_font_destroy(font);
}
void HarfbuzzShaper::setFallbacks(const std::vector<Rasterizer*>& fallbacks)
{
for (size_t i = 1; i < hbFonts.size(); i++)
hb_font_destroy(hbFonts[i]);
TextShaper::setFallbacks(fallbacks);
hbFonts.resize(rasterizers.size());
for (size_t i = 1; i < hbFonts.size(); i++)
hbFonts[i] = hb_ft_font_create_referenced((FT_Face)rasterizers[i]->getHandle());
}
void HarfbuzzShaper::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());
offset.y += getBaseline();
Vector2 curpos = offset;
hb_buffer_t *hbbuffer = hb_buffer_create();
hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int) range.getOffset(), (int) range.getSize());
// TODO: Expose APIs for direction and script?
hb_buffer_guess_segment_properties(hbbuffer);
hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
int glyphcount = (int) hb_buffer_get_length(hbbuffer);
hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
if (positions)
positions->reserve(positions->size() + glyphcount);
if (rasterizers.size() > 1)
{
std::vector<Range> fallbackranges;
for (int i = 0; i < glyphcount; i++)
{
bool adding = false;
if (glyphinfos[i].codepoint == 0)
{
if (fallbackranges.empty() || fallbackranges.back().getMax() != glyphinfos[i-1].cluster)
fallbackranges.push_back(Range(glyphinfos[i].cluster, 1));
else
fallbackranges.back().encapsulate(glyphinfos[i].cluster);
}
}
if (!fallbackranges.empty())
{
for (size_t rasti = 1; rasti < rasterizers.size(); rasti++)
{
hb_buffer_t* hbbuffer = hb_buffer_create();
hb_buffer_destroy(hbbuffer);
}
}
}
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);
}
}
int maxwidth = (int)curpos.x;
// TODO: fallbacks
// TODO: use spaces for tab
for (int i = 0; i < glyphcount; i++)
{
const hb_glyph_info_t& info = glyphinfos[i];
const hb_glyph_position_t& glyphpos = glyphpositions[i];
// TODO: this doesn't handle situations where the user inserted a color
// change in the middle of some characters that get combined into a single
// cluster.
if (colors && colorindex < ncolors && codepoints.colors[colorindex].index == info.cluster)
{
colorToAdd.set(codepoints.colors[colorindex].color);
colorindex++;
}
uint32 clustercodepoint = codepoints.cps[info.cluster];
// Harfbuzz doesn't handle newlines itself, but it does leave them in
// the glyph list so we can do it manually.
if (clustercodepoint == '\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;
continue;
}
// Ignore carriage returns
if (clustercodepoint == '\r')
continue;
if (colorToAdd.hasValue && colors && positions)
{
IndexedColor c = {colorToAdd.value, positions->size()};
colors->push_back(c);
colorToAdd.clear();
}
if (positions)
{
// Despite the name this is a glyph index at this point.
GlyphIndex gindex = { info.codepoint, 0 };
GlyphPosition p = { curpos, gindex };
// Harfbuzz position coordinate systems are based on the given font.
// Freetype uses 26.6 fixed point coordinates, so harfbuzz does too.
p.position.x += floorf((glyphpos.x_offset >> 6) / dpiScales[0] + 0.5f);
p.position.y += floorf((glyphpos.y_offset >> 6) / dpiScales[0] + 0.5f);
positions->push_back(p);
}
curpos.x += floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f);
curpos.y += floorf((glyphpos.y_advance >> 6) / dpiScales[0] + 0.5f);
// Account for extra spacing given to space characters.
if (clustercodepoint == ' ' && extraspacing != 0.0f)
curpos.x = floorf(curpos.x + extraspacing);
}
hb_buffer_destroy(hbbuffer);
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 HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width)
{
if (!range.isValid())
range = Range(0, codepoints.cps.size());
hb_buffer_t* hbbuffer = hb_buffer_create();
hb_buffer_add_codepoints(hbbuffer, codepoints.cps.data(), codepoints.cps.size(), (unsigned int)range.getOffset(), (int)range.getSize());
// TODO: Expose APIs for direction and script?
hb_buffer_guess_segment_properties(hbbuffer);
hb_shape(hbFonts[0], hbbuffer, nullptr, 0);
int glyphcount = (int)hb_buffer_get_length(hbbuffer);
hb_glyph_info_t* glyphinfos = hb_buffer_get_glyph_infos(hbbuffer, nullptr);
hb_glyph_position_t* glyphpositions = hb_buffer_get_glyph_positions(hbbuffer, nullptr);
float w = 0.0f;
float outwidth = 0.0f;
float widthbeforelastspace = 0.0f;
int wrapindex = -1;
int lastspaceindex = -1;
uint32 prevcodepoint = 0;
for (int i = 0; i < glyphcount; i++)
{
const hb_glyph_info_t& info = glyphinfos[i];
const hb_glyph_position_t& glyphpos = glyphpositions[i];
uint32 clustercodepoint = codepoints.cps[info.cluster];
if (clustercodepoint == '\r')
{
prevcodepoint = clustercodepoint;
continue;
}
float newwidth = w + floorf((glyphpos.x_advance >> 6) / dpiScales[0] + 0.5f);
// Only wrap when there's a non-space character.
if (newwidth > wraplimit && clustercodepoint != ' ')
{
// 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 (clustercodepoint == ' ')
{
lastspaceindex = info.cluster;
if (prevcodepoint != ' ')
widthbeforelastspace = w;
}
else
outwidth = newwidth;
w = newwidth;
prevcodepoint = clustercodepoint;
wrapindex = info.cluster;
}
hb_buffer_destroy(hbbuffer);
if (width)
*width = outwidth;
return wrapindex;
}
} // freetype
} // font
} // love
@@ -0,0 +1,59 @@
/**
* 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.
**/
#pragma once
// LOVE
#include "font/TextShaper.h"
extern "C"
{
typedef struct hb_font_t hb_font_t;
}
namespace love
{
namespace font
{
namespace freetype
{
class TrueTypeRasterizer;
class HarfbuzzShaper : public love::font::TextShaper
{
public:
HarfbuzzShaper(TrueTypeRasterizer *rasterizer);
virtual ~HarfbuzzShaper();
void setFallbacks(const std::vector<Rasterizer*>& fallbacks) override;
void computeGlyphPositions(const ColoredCodepoints &codepoints, Range range, Vector2 offset, float extraspacing, std::vector<GlyphPosition> *positions, std::vector<IndexedColor> *colors, TextInfo *info) override;
int computeWordWrapIndex(const ColoredCodepoints& codepoints, Range range, float wraplimit, float *width) override;
private:
std::vector<hb_font_t *> hbFonts;
}; // HarfbuzzShaper
} // freetype
} // font
} // love
@@ -20,6 +20,7 @@
// LOVE
#include "TrueTypeRasterizer.h"
#include "HarfbuzzShaper.h"
#include "common/Exception.h"
// C
@@ -75,7 +76,30 @@ int TrueTypeRasterizer::getLineHeight() const
return (int)(getHeight() * 1.25);
}
GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
int TrueTypeRasterizer::getGlyphSpacing(uint32 glyph) const
{
FT_Glyph ftglyph;
FT_Error err = FT_Err_Ok;
FT_UInt loadoption = hintingToLoadOption(hinting);
// Initialize
err = FT_Load_Glyph(face, FT_Get_Char_Index(face, glyph), FT_LOAD_DEFAULT | loadoption);
if (err != FT_Err_Ok)
return 0;
err = FT_Get_Glyph(face->glyph, &ftglyph);
if (err != FT_Err_Ok)
return 0;
return (int)(ftglyph->advance.x >> 16);
}
int TrueTypeRasterizer::getGlyphIndex(uint32 glyph) const
{
return FT_Get_Char_Index(face, glyph);
}
GlyphData *TrueTypeRasterizer::getGlyphDataForIndex(int index) const
{
love::font::GlyphMetrics glyphMetrics = {};
FT_Glyph ftglyph;
@@ -84,7 +108,7 @@ GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
FT_UInt loadoption = hintingToLoadOption(hinting);
// Initialize
err = FT_Load_Glyph(face, FT_Get_Char_Index(face, glyph), FT_LOAD_DEFAULT | loadoption);
err = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT | loadoption);
if (err != FT_Err_Ok)
throw love::Exception("TrueType Font glyph error: FT_Load_Glyph failed (0x%x)", err);
@@ -104,7 +128,7 @@ GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
throw love::Exception("TrueType Font glyph error: FT_Glyph_To_Bitmap failed (0x%x)", err);
FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph) ftglyph;
FT_Bitmap &bitmap = bitmap_glyph->bitmap; //just to make things easier
const FT_Bitmap &bitmap = bitmap_glyph->bitmap; //just to make things easier
// Get metrics
glyphMetrics.bearingX = bitmap_glyph->left;
@@ -113,7 +137,8 @@ GlyphData *TrueTypeRasterizer::getGlyphData(uint32 glyph) const
glyphMetrics.width = bitmap.width;
glyphMetrics.advance = (int) (ftglyph->advance.x >> 16);
GlyphData *glyphData = new GlyphData(glyph, glyphMetrics, PIXELFORMAT_LA8_UNORM);
// TODO: https://stackoverflow.com/questions/60526004/how-to-get-glyph-unicode-using-freetype/69730502#69730502
GlyphData *glyphData = new GlyphData(0, glyphMetrics, PIXELFORMAT_LA8_UNORM);
const uint8 *pixels = bitmap.buffer;
uint8 *dest = (uint8 *) glyphData->getData();
@@ -186,6 +211,11 @@ Rasterizer::DataType TrueTypeRasterizer::getDataType() const
return DATA_TRUETYPE;
}
TextShaper *TrueTypeRasterizer::newTextShaper()
{
return new HarfbuzzShaper(this);
}
bool TrueTypeRasterizer::accepts(FT_Library library, love::Data *data)
{
const FT_Byte *fbase = (const FT_Byte *) data->getData();
@@ -49,11 +49,16 @@ public:
// Implement Rasterizer
int getLineHeight() const override;
GlyphData *getGlyphData(uint32 glyph) const override;
int getGlyphSpacing(uint32 glyph) const override;
int getGlyphIndex(uint32 glyph) const override;
GlyphData *getGlyphDataForIndex(int index) const override;
int getGlyphCount() const override;
bool hasGlyph(uint32 glyph) const override;
float getKerning(uint32 leftglyph, uint32 rightglyph) const override;
DataType getDataType() const override;
TextShaper *newTextShaper() override;
ptrdiff_t getHandle() const override { return (ptrdiff_t) face; }
static bool accepts(FT_Library library, love::Data *data);