mirror of
https://github.com/love2d/love.git
synced 2026-08-20 20:49:54 +02:00
Improve truetype font text positioning with fractional glyph advances and DPI-scaled coordinates.
Disallow Font:setFallbacks for fonts that have different DPI scales. It's still too easy to render text at a non-integer pixel when fractional DPI scales are used, so maybe a more thorough redesign would be useful. Fixes #2318. Fixes #2168.
This commit is contained in:
@@ -84,7 +84,7 @@ love::Type TextShaper::type("TextShaper", &Object::type);
|
|||||||
|
|
||||||
TextShaper::TextShaper(Rasterizer *rasterizer)
|
TextShaper::TextShaper(Rasterizer *rasterizer)
|
||||||
: rasterizers{rasterizer}
|
: rasterizers{rasterizer}
|
||||||
, dpiScales{rasterizer->getDPIScale()}
|
, dpiScale(rasterizer->getDPIScale())
|
||||||
, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
|
, height(floorf(rasterizer->getHeight() / rasterizer->getDPIScale() + 0.5f))
|
||||||
, pixelHeight(rasterizer->getHeight())
|
, pixelHeight(rasterizer->getHeight())
|
||||||
, lineHeight(1)
|
, lineHeight(1)
|
||||||
@@ -371,7 +371,10 @@ void TextShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
|
|||||||
for (Rasterizer *r : fallbacks)
|
for (Rasterizer *r : fallbacks)
|
||||||
{
|
{
|
||||||
if (r->getDataType() != rasterizers[0]->getDataType())
|
if (r->getDataType() != rasterizers[0]->getDataType())
|
||||||
throw love::Exception("Font fallbacks must be of the same font type.");
|
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.
|
// Clear caches.
|
||||||
@@ -379,13 +382,9 @@ void TextShaper::setFallbacks(const std::vector<Rasterizer*> &fallbacks)
|
|||||||
glyphAdvances.clear();
|
glyphAdvances.clear();
|
||||||
|
|
||||||
rasterizers.resize(1);
|
rasterizers.resize(1);
|
||||||
dpiScales.resize(1);
|
|
||||||
|
|
||||||
for (Rasterizer *r : fallbacks)
|
for (Rasterizer *r : fallbacks)
|
||||||
{
|
|
||||||
rasterizers.push_back(r);
|
rasterizers.push_back(r);
|
||||||
dpiScales.push_back(r->getDPIScale());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // font
|
} // font
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ protected:
|
|||||||
static inline bool isWhitespace(uint32 codepoint) { return codepoint == ' ' || codepoint == '\t'; }
|
static inline bool isWhitespace(uint32 codepoint) { return codepoint == ' ' || codepoint == '\t'; }
|
||||||
|
|
||||||
std::vector<StrongRef<Rasterizer>> rasterizers;
|
std::vector<StrongRef<Rasterizer>> rasterizers;
|
||||||
std::vector<float> dpiScales;
|
float dpiScale;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,24 @@ namespace font
|
|||||||
namespace freetype
|
namespace freetype
|
||||||
{
|
{
|
||||||
|
|
||||||
|
static inline float fixed26_6ToFloat(int32 v)
|
||||||
|
{
|
||||||
|
return (v >> 6) + (v & 63) / 64.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32 floatToFixed26_6(float v)
|
||||||
|
{
|
||||||
|
float integer = 0;
|
||||||
|
float frac = modf(v, &integer);
|
||||||
|
|
||||||
|
return (int32)(((uint32)integer << 6) | (uint32)(frac * 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
static float snapToPixel(float v, float dpiScale, float dpiScaleInverse)
|
||||||
|
{
|
||||||
|
return roundf(v * dpiScale) * dpiScaleInverse;
|
||||||
|
}
|
||||||
|
|
||||||
HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
|
HarfbuzzShaper::HarfbuzzShaper(TrueTypeRasterizer *rasterizer)
|
||||||
: TextShaper(rasterizer)
|
: TextShaper(rasterizer)
|
||||||
, spaceGlyphIndex()
|
, spaceGlyphIndex()
|
||||||
@@ -97,8 +115,8 @@ void HarfbuzzShaper::updateSpacesForTabInfo()
|
|||||||
{
|
{
|
||||||
spaceGlyphIndex.index = glyphid;
|
spaceGlyphIndex.index = glyphid;
|
||||||
spaceGlyphIndex.rasterizerIndex = i;
|
spaceGlyphIndex.rasterizerIndex = i;
|
||||||
tabSpacesAdvanceX = hb_font_get_glyph_h_advance(hbfont, glyphid) * SPACES_PER_TAB;
|
tabSpacesAdvanceX = floatToFixed26_6(fixed26_6ToFloat(hb_font_get_glyph_h_advance(hbfont, glyphid)) * SPACES_PER_TAB);
|
||||||
tabSpacesAdvanceY = hb_font_get_glyph_v_advance(hbfont, glyphid) * SPACES_PER_TAB;
|
tabSpacesAdvanceY = floatToFixed26_6(fixed26_6ToFloat(hb_font_get_glyph_v_advance(hbfont, glyphid)) * SPACES_PER_TAB);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,6 +259,8 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float dpiScaleInverse = 1.0f / dpiScale;
|
||||||
|
|
||||||
std::vector<BufferRange> bufferranges;
|
std::vector<BufferRange> bufferranges;
|
||||||
computeBufferRanges(codepoints, range, bufferranges);
|
computeBufferRanges(codepoints, range, bufferranges);
|
||||||
|
|
||||||
@@ -317,14 +337,18 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
|
|||||||
|
|
||||||
// Harfbuzz position coordinate systems are based on the given font.
|
// Harfbuzz position coordinate systems are based on the given font.
|
||||||
// Freetype uses 26.6 fixed point coordinates, so harfbuzz does too.
|
// Freetype uses 26.6 fixed point coordinates, so harfbuzz does too.
|
||||||
p.position.x += (glyphpos.x_offset >> 6) / dpiScales[bufferrange.index];
|
p.position.x += fixed26_6ToFloat(glyphpos.x_offset) * dpiScaleInverse;
|
||||||
p.position.y += (glyphpos.y_offset >> 6) / dpiScales[bufferrange.index];
|
p.position.y += fixed26_6ToFloat(glyphpos.y_offset) * dpiScaleInverse;
|
||||||
|
|
||||||
|
// Snap position to the current font's source pixel grid.
|
||||||
|
p.position.x = snapToPixel(p.position.x, dpiScale, dpiScaleInverse);
|
||||||
|
p.position.y = snapToPixel(p.position.y, dpiScale, dpiScaleInverse);
|
||||||
|
|
||||||
positions->push_back(p);
|
positions->push_back(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
curpos.x += (glyphpos.x_advance >> 6) / dpiScales[bufferrange.index];
|
curpos.x += fixed26_6ToFloat(glyphpos.x_advance) * dpiScaleInverse;
|
||||||
curpos.y += (glyphpos.y_advance >> 6) / dpiScales[bufferrange.index];
|
curpos.y += fixed26_6ToFloat(glyphpos.y_advance) * dpiScaleInverse;
|
||||||
|
|
||||||
// Account for extra spacing given to space characters.
|
// Account for extra spacing given to space characters.
|
||||||
if (clustercodepoint == ' ' && extraspacing != 0.0f)
|
if (clustercodepoint == ' ' && extraspacing != 0.0f)
|
||||||
@@ -340,8 +364,8 @@ void HarfbuzzShaper::computeGlyphPositions(const ColoredCodepoints &codepoints,
|
|||||||
|
|
||||||
if (info != nullptr)
|
if (info != nullptr)
|
||||||
{
|
{
|
||||||
info->width = maxwidth - offset.x;
|
info->width = snapToPixel(maxwidth - offset.x, dpiScale, dpiScaleInverse);
|
||||||
info->height = curpos.y - offset.y;
|
info->height = snapToPixel(curpos.y - offset.y, dpiScale, dpiScaleInverse);
|
||||||
if (curpos.x > offset.x)
|
if (curpos.x > offset.x)
|
||||||
info->height += getCombinedHeight();
|
info->height += getCombinedHeight();
|
||||||
}
|
}
|
||||||
@@ -359,6 +383,8 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Ra
|
|||||||
|
|
||||||
uint32 prevcodepoint = 0;
|
uint32 prevcodepoint = 0;
|
||||||
|
|
||||||
|
const float dpiScaleInverse = 1.0f / dpiScale;
|
||||||
|
|
||||||
std::vector<BufferRange> bufferranges;
|
std::vector<BufferRange> bufferranges;
|
||||||
computeBufferRanges(codepoints, range, bufferranges);
|
computeBufferRanges(codepoints, range, bufferranges);
|
||||||
|
|
||||||
@@ -393,7 +419,7 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Ra
|
|||||||
glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0;
|
glyphpos.y_advance = HB_DIRECTION_IS_VERTICAL(direction) ? tabSpacesAdvanceY : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
float newwidth = w + (glyphpos.x_advance >> 6) / dpiScales[bufferrange.index];
|
float newwidth = w + fixed26_6ToFloat(glyphpos.x_advance) * dpiScaleInverse;
|
||||||
|
|
||||||
// Don't count trailing spaces in the output width.
|
// Don't count trailing spaces in the output width.
|
||||||
if (isWhitespace(clustercodepoint))
|
if (isWhitespace(clustercodepoint))
|
||||||
@@ -407,7 +433,7 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Ra
|
|||||||
firstindexafterspace = info.cluster;
|
firstindexafterspace = info.cluster;
|
||||||
|
|
||||||
// Only wrap when there's a non-space character.
|
// Only wrap when there's a non-space character.
|
||||||
if (newwidth > wraplimit)
|
if (snapToPixel(newwidth, dpiScale, dpiScaleInverse) > wraplimit)
|
||||||
{
|
{
|
||||||
// If this is the first character, wrap from the next one instead of this one.
|
// If this is the first character, wrap from the next one instead of this one.
|
||||||
int wrapindex = (int)info.cluster > (int)range.first ? (int)info.cluster : (int)range.first + 1;
|
int wrapindex = (int)info.cluster > (int)range.first ? (int)info.cluster : (int)range.first + 1;
|
||||||
@@ -420,7 +446,7 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Ra
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (width)
|
if (width)
|
||||||
*width = outwidth;
|
*width = snapToPixel(outwidth, dpiScale, dpiScaleInverse);
|
||||||
|
|
||||||
return wrapindex;
|
return wrapindex;
|
||||||
}
|
}
|
||||||
@@ -434,7 +460,7 @@ int HarfbuzzShaper::computeWordWrapIndex(const ColoredCodepoints &codepoints, Ra
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (width)
|
if (width)
|
||||||
*width = outwidth;
|
*width = snapToPixel(outwidth, dpiScale, dpiScaleInverse);
|
||||||
|
|
||||||
// There wasn't any wrap in the middle of the range.
|
// There wasn't any wrap in the middle of the range.
|
||||||
return (int) range.last + 1;
|
return (int) range.last + 1;
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ private:
|
|||||||
std::vector<hb_buffer_t *> hbBuffers;
|
std::vector<hb_buffer_t *> hbBuffers;
|
||||||
|
|
||||||
GlyphIndex spaceGlyphIndex;
|
GlyphIndex spaceGlyphIndex;
|
||||||
int tabSpacesAdvanceX;
|
int32 tabSpacesAdvanceX;
|
||||||
int tabSpacesAdvanceY;
|
int32 tabSpacesAdvanceY;
|
||||||
|
|
||||||
}; // HarfbuzzShaper
|
}; // HarfbuzzShaper
|
||||||
|
|
||||||
|
|||||||
@@ -608,12 +608,12 @@ void Font::printf(graphics::Graphics *gfx, const std::vector<love::font::Colored
|
|||||||
printv(gfx, m, drawcommands, vertices);
|
printv(gfx, m, drawcommands, vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Font::getWidth(const std::string &str)
|
float Font::getWidth(const std::string &str)
|
||||||
{
|
{
|
||||||
return shaper->getWidth(str);
|
return shaper->getWidth(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Font::getWidth(uint32 glyph)
|
float Font::getWidth(uint32 glyph)
|
||||||
{
|
{
|
||||||
return shaper->getGlyphAdvance(glyph);
|
return shaper->getGlyphAdvance(glyph);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,12 +99,12 @@ public:
|
|||||||
*
|
*
|
||||||
* @param str A string of text.
|
* @param str A string of text.
|
||||||
**/
|
**/
|
||||||
int getWidth(const std::string &str);
|
float getWidth(const std::string &str);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the width of the passed glyph.
|
* Returns the width of the passed glyph.
|
||||||
**/
|
**/
|
||||||
int getWidth(uint32 glyph);
|
float getWidth(uint32 glyph);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the maximal width of a wrapped string
|
* Returns the maximal width of a wrapped string
|
||||||
|
|||||||
@@ -93,12 +93,12 @@ int w_Font_getWidth(lua_State *L)
|
|||||||
if (lua_type(L, 2) == LUA_TSTRING)
|
if (lua_type(L, 2) == LUA_TSTRING)
|
||||||
{
|
{
|
||||||
const char *str = luaL_checkstring(L, 2);
|
const char *str = luaL_checkstring(L, 2);
|
||||||
luax_catchexcept(L, [&](){ lua_pushinteger(L, t->getWidth(str)); });
|
luax_catchexcept(L, [&](){ lua_pushnumber(L, t->getWidth(str)); });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
uint32 glyph = (uint32) luaL_checknumber(L, 2);
|
uint32 glyph = (uint32) luaL_checknumber(L, 2);
|
||||||
luax_catchexcept(L, [&](){ lua_pushinteger(L, t->getWidth(glyph)); });
|
luax_catchexcept(L, [&](){ lua_pushnumber(L, t->getWidth(glyph)); });
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user