Util: Add text metrics functions

This commit is contained in:
Vicki Pfau 2025-06-08 22:19:02 -07:00
parent 467768d414
commit 7fa4acf1c7
5 changed files with 231 additions and 95 deletions

View File

@ -10,6 +10,8 @@
CXX_GUARD_START
#include <mgba-util/geometry.h>
#ifdef COLOR_16_BIT
typedef uint16_t mColor;
#define BYTES_PER_PIXEL 2
@ -112,6 +114,16 @@ struct mPainter {
uint32_t fillColor;
};
#ifdef USE_FREETYPE
#define mFONT_FRACT_BITS 6
struct mTextRunMetrics {
int height;
int baseline;
int width;
};
#endif
enum mAlignment {
mALIGN_LEFT = 0x01,
mALIGN_HCENTER = 0x02,
@ -170,7 +182,8 @@ void mFontDestroy(struct mFont*);
unsigned mFontSize(const struct mFont*);
void mFontSetSize(struct mFont*, unsigned px);
int mFontSpanWidth(struct mFont*, const char* text);
const char* mFontRunMetrics(struct mFont*, const char* text, struct mTextRunMetrics* out);
void mFontTextBoxSize(struct mFont*, const char* text, int lineSpacing, struct mSize* out);
void mPainterDrawText(struct mPainter*, const char* text, int x, int y, enum mAlignment);
#endif

View File

@ -11,24 +11,24 @@ state.painter:setStrokeColor(0xFF000000)
state.painter:setBlend(true)
state.painter:setFillColor(0xFFFFFFFF)
state.painter:drawText("Top left", 0, 0, C.ALIGN.TOP | C.ALIGN.LEFT)
state.painter:drawText("Top\nleft", 0, 0, C.ALIGN.TOP | C.ALIGN.LEFT)
state.painter:setFillColor(0xFF00FFFF)
state.painter:drawText("Top center", state.width / 2, 0, C.ALIGN.TOP | C.ALIGN.HCENTER)
state.painter:drawText("Top\ncenter", state.width / 2, 0, C.ALIGN.TOP | C.ALIGN.HCENTER)
state.painter:setFillColor(0xFFFF00FF)
state.painter:drawText("Top right", state.width, 0, C.ALIGN.TOP | C.ALIGN.RIGHT)
state.painter:drawText("Top\nright", state.width, 0, C.ALIGN.TOP | C.ALIGN.RIGHT)
state.painter:setFillColor(0xFFFFFF00)
state.painter:drawText("Center left", 0, state.height / 2, C.ALIGN.VCENTER | C.ALIGN.LEFT)
state.painter:drawText("Center\nleft", 0, state.height / 2, C.ALIGN.VCENTER | C.ALIGN.LEFT)
state.painter:setFillColor(0xFFFF0000)
state.painter:drawText("Center", state.width / 2, state.height / 2, C.ALIGN.VCENTER | C.ALIGN.HCENTER)
state.painter:drawText("Center\ncenter", state.width / 2, state.height / 2, C.ALIGN.VCENTER | C.ALIGN.HCENTER)
state.painter:setFillColor(0xFF00FF00)
state.painter:drawText("Center right", state.width, state.height / 2, C.ALIGN.VCENTER | C.ALIGN.RIGHT)
state.painter:drawText("Center\nright", state.width, state.height / 2, C.ALIGN.VCENTER | C.ALIGN.RIGHT)
state.painter:setFillColor(0xFF0000FF)
state.painter:drawText("Bottom left", 0, state.height, C.ALIGN.BOTTOM | C.ALIGN.LEFT)
state.painter:drawText("Bottom\nleft", 0, state.height, C.ALIGN.BOTTOM | C.ALIGN.LEFT)
state.painter:setFillColor(0xFF808080)
state.painter:drawText("Bottom center", state.width / 2, state.height, C.ALIGN.BOTTOM | C.ALIGN.HCENTER)
state.painter:drawText("Bottom\ncenter", state.width / 2, state.height, C.ALIGN.BOTTOM | C.ALIGN.HCENTER)
state.painter:setStrokeColor(0xFFFFFFFF)
state.painter:setFillColor(0xFF000000)
state.painter:drawText("Bottom right", state.width, state.height, C.ALIGN.BOTTOM | C.ALIGN.RIGHT)
state.painter:drawText("Bottom\nright", state.width, state.height, C.ALIGN.BOTTOM | C.ALIGN.RIGHT)
state.overlay:update()

View File

@ -0,0 +1,20 @@
local state = {}
state.width = canvas:screenWidth()
state.height = canvas:screenHeight()
state.overlay = canvas:newLayer(state.width, state.height)
state.painter = image.newPainter(state.overlay.image)
state.painter:loadFont(script.dir .. "/SourceSans3-Regular.otf")
state.painter:setFontSize(10.5)
state.painter:setFill(true)
state.painter:setBlend(true)
state.text = "Sphinx of black quartz judge my vow"
state.metrics = state.painter:textRunMetrics(state.text)
state.painter:setFillColor(0x80FF0000)
state.painter:drawRectangle(0, 0, state.metrics:width(), state.metrics:ascender())
state.painter:setFillColor(0x800000FF)
state.painter:drawRectangle(0, state.metrics:ascender(), state.metrics:width(), state.metrics:descender())
state.painter:setFillColor(0xFFFFFFFF)
state.painter:drawText(state.text, 0, 0)
state.overlay:update()

View File

@ -13,6 +13,9 @@ struct mScriptPainter {
mSCRIPT_DECLARE_STRUCT(mPainter);
mSCRIPT_DECLARE_STRUCT(mScriptPainter);
#ifdef USE_FREETYPE
mSCRIPT_DECLARE_STRUCT(mTextRunMetrics);
#endif
static struct mScriptValue* _mImageNew(unsigned width, unsigned height) {
// For various reasons, it's probably a good idea to limit the maximum image size scripts can make
@ -140,15 +143,27 @@ void _mPainterSetFontSize(struct mPainter* painter, float pt) {
if (!painter->font) {
return;
}
mFontSetSize(painter->font, pt * 64);
mFontSetSize(painter->font, pt * (1 << mFONT_FRACT_BITS));
}
struct mScriptValue* _mPainterTextRunMetrics(struct mPainter* painter, const char* text) {
struct mTextRunMetrics* metrics = malloc(sizeof(*metrics));
mFontRunMetrics(painter->font, text, metrics);
float _mPainterTextSpanWidth(struct mPainter* painter, const char* text) {
if (!painter->font) {
return 0;
}
return mFontSpanWidth(painter->font, text) / 64.f;
struct mScriptValue* result = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mTextRunMetrics));
result->value.opaque = metrics;
result->flags = mSCRIPT_VALUE_FLAG_DEINIT;
return result;
}
struct mScriptValue* _mPainterTextBoxSize(struct mPainter* painter, const char* text) {
struct mSize* size = malloc(sizeof(*size));
mFontTextBoxSize(painter->font, text, 0, size);
struct mScriptValue* result = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mSize));
result->value.opaque = size;
result->flags = mSCRIPT_VALUE_FLAG_DEINIT;
return result;
}
#endif
@ -185,10 +200,19 @@ mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawLine, mPainterDrawLine, 4, S32,
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawCircle, mPainterDrawCircle, 3, S32, x, S32, y, S32, diameter);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawMask, mPainterDrawMask, 3, CS(mImage), mask, S32, x, S32, y);
#ifdef USE_FREETYPE
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawText, mPainterDrawText, 4, CHARP, text, S32, x, S32, y, S32, alignment);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD_WITH_DEFAULTS(mPainter, drawText, mPainterDrawText, 4, CHARP, text, S32, x, S32, y, S32, alignment);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, loadFont, _mPainterLoadFont, 1, CHARP, path);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, setFontSize, _mPainterSetFontSize, 1, U32, pt);
mSCRIPT_DECLARE_STRUCT_METHOD(mPainter, F32, textSpanWidth, _mPainterTextSpanWidth, 1, CHARP, text);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, setFontSize, _mPainterSetFontSize, 1, F32, pt);
mSCRIPT_DECLARE_STRUCT_METHOD(mPainter, W(mTextRunMetrics), textRunMetrics, _mPainterTextRunMetrics, 1, CHARP, text);
mSCRIPT_DECLARE_STRUCT_METHOD(mPainter, W(mSize), textBoxSize, _mPainterTextBoxSize, 1, CHARP, text);
mSCRIPT_DEFINE_STRUCT_BINDING_DEFAULTS(mPainter, drawText)
mSCRIPT_NO_DEFAULT,
mSCRIPT_NO_DEFAULT,
mSCRIPT_NO_DEFAULT,
mSCRIPT_S32(mALIGN_TOP | mALIGN_LEFT)
mSCRIPT_DEFINE_DEFAULTS_END;
#endif
mSCRIPT_DEFINE_STRUCT(mPainter)
@ -225,8 +249,16 @@ mSCRIPT_DEFINE_STRUCT(mPainter)
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, loadFont)
mSCRIPT_DEFINE_DOCSTRING("Set the font size")
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, setFontSize)
mSCRIPT_DEFINE_DOCSTRING("Get the pixel width of a span of text in the current font")
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, textSpanWidth)
mSCRIPT_DEFINE_DOCSTRING(
"Get the struct::mTextRunMetrics for the first line of a given string rendered in the current "
"font. If you want the bounding box for multiple lines, use struct::mPainter.textBoxSize instead."
)
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, textRunMetrics)
mSCRIPT_DEFINE_DOCSTRING(
"Get the bounding box size for the given string rendered in the current font. This "
"will take into account line breaks, unlike struct::mPainter.textRunMetrics."
)
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, textBoxSize)
#endif
mSCRIPT_DEFINE_END;
@ -240,6 +272,43 @@ mSCRIPT_DEFINE_STRUCT(mScriptPainter)
mSCRIPT_DEFINE_STRUCT_CAST_TO_MEMBER(mScriptPainter, S(mPainter), _painter)
mSCRIPT_DEFINE_END;
#ifdef USE_FREETYPE
float _mTextRunHeight(const struct mTextRunMetrics* metrics) {
return metrics->height / (float) (1 << mFONT_FRACT_BITS);
}
float _mTextRunWidth(const struct mTextRunMetrics* metrics) {
return metrics->width / (float) (1 << mFONT_FRACT_BITS);
}
float _mTextRunDescender(const struct mTextRunMetrics* metrics) {
return metrics->baseline / (float) (1 << mFONT_FRACT_BITS);
}
float _mTextRunAscender(const struct mTextRunMetrics* metrics) {
return (metrics->height - metrics->baseline) / (float) (1 << mFONT_FRACT_BITS);
}
mSCRIPT_DECLARE_STRUCT_C_METHOD(mTextRunMetrics, F32, height, _mTextRunHeight, 0);
mSCRIPT_DECLARE_STRUCT_C_METHOD(mTextRunMetrics, F32, width, _mTextRunWidth, 0);
mSCRIPT_DECLARE_STRUCT_C_METHOD(mTextRunMetrics, F32, descender, _mTextRunDescender, 0);
mSCRIPT_DECLARE_STRUCT_C_METHOD(mTextRunMetrics, F32, ascender, _mTextRunAscender, 0);
mSCRIPT_DEFINE_STRUCT(mTextRunMetrics)
mSCRIPT_DEFINE_CLASS_DOCSTRING(
"Metrics for the size of a run of text. Generally, a run will represent up to a single line of text."
)
mSCRIPT_DEFINE_DOCSTRING("Get the height of the run of text, in pixels")
mSCRIPT_DEFINE_STRUCT_METHOD(mTextRunMetrics, height)
mSCRIPT_DEFINE_DOCSTRING("Get the width of the run of text, in pixels")
mSCRIPT_DEFINE_STRUCT_METHOD(mTextRunMetrics, width)
mSCRIPT_DEFINE_DOCSTRING("Get the distance from the baseline to the bottom of the line, in pixels")
mSCRIPT_DEFINE_STRUCT_METHOD(mTextRunMetrics, descender)
mSCRIPT_DEFINE_DOCSTRING("Get the distance from the baseline to the top of the line, in pixels")
mSCRIPT_DEFINE_STRUCT_METHOD(mTextRunMetrics, ascender)
mSCRIPT_DEFINE_END;
#endif
void mScriptContextAttachImage(struct mScriptContext* context) {
mScriptContextExportNamespace(context, "image", (struct mScriptKVPair[]) {
mSCRIPT_KV_PAIR(new, &mImageNew_Binding),

View File

@ -56,7 +56,7 @@ struct mFont* mFontOpen(const char* path) {
struct mFont* font = calloc(1, sizeof(*font));
font->face = face;
mFontSetSize(font, 8 * 64);
mFontSetSize(font, 8 << mFONT_FRACT_BITS);
return font;
}
@ -80,31 +80,121 @@ void mFontSetSize(struct mFont* font, unsigned pt) {
}
int mFontSpanWidth(struct mFont* font, const char* text) {
struct mTextRunMetrics metrics;
mFontRunMetrics(font, text, &metrics);
return metrics.width;
}
const char* mFontRunMetrics(struct mFont* font, const char* text, struct mTextRunMetrics* out) {
FT_Face face = font->face;
out->height = face->size->metrics.ascender - face->size->metrics.descender;
out->baseline = -face->size->metrics.descender;
uint32_t lastGlyph = 0;
int width = 0;
while (*text) {
uint32_t glyph = utf8Char((const char**) &text, NULL);
uint32_t codepoint = utf8Char((const char**) &text, NULL);
if (FT_Load_Char(face, glyph, FT_LOAD_DEFAULT)) {
if (codepoint == '\n') {
out->width = width;
return text;
}
if (FT_Load_Char(face, codepoint, FT_LOAD_DEFAULT)) {
lastGlyph = 0;
continue;
}
FT_Vector kerning = {0};
FT_Get_Kerning(face, lastGlyph, glyph, FT_KERNING_DEFAULT, &kerning);
FT_Get_Kerning(face, lastGlyph, codepoint, FT_KERNING_DEFAULT, &kerning);
width += kerning.x;
width += face->glyph->advance.x;
lastGlyph = glyph;
lastGlyph = codepoint;
}
return width;
out->width = width;
return NULL;
}
void mFontTextBoxSize(struct mFont* font, const char* text, int lineSpacing, struct mSize* out) {
int width = 0;
int height = 0;
do {
struct mTextRunMetrics metrics;
text = mFontRunMetrics(font, text, &metrics);
if (metrics.width > width) {
width = metrics.width;
}
height += metrics.height + lineSpacing;
} while (text);
out->width = width;
out->height = height;
}
static const char* mPainterDrawTextRun(struct mPainter* painter, const char* text, int x, int y, enum mAlignment alignment, uint8_t sdfThreshold) {
FT_Face face = painter->font->face;
struct mTextRunMetrics metrics;
mFontRunMetrics(painter->font, text, &metrics);
switch (alignment & mALIGN_HORIZONTAL) {
case mALIGN_LEFT:
default:
break;
case mALIGN_HCENTER:
x -= metrics.width >> 1;
break;
case mALIGN_RIGHT:
x -= metrics.width;
break;
}
uint32_t lastGlyph = 0;
while (*text) {
uint32_t codepoint = utf8Char((const char**) &text, NULL);
if (codepoint == '\n') {
return text;
}
if (FT_Load_Char(face, codepoint, FT_LOAD_DEFAULT)) {
lastGlyph = 0;
continue;
}
if (FT_Render_Glyph(face->glyph, sdfThreshold ? FT_RENDER_MODE_SDF : FT_RENDER_MODE_NORMAL)) {
lastGlyph = 0;
continue;
}
struct mImage image;
_makeTemporaryImage(&image, &face->glyph->bitmap);
FT_Vector kerning = {0};
FT_Get_Kerning(face, lastGlyph, codepoint, FT_KERNING_DEFAULT, &kerning);
x += kerning.x;
y += kerning.y;
if (sdfThreshold) {
mPainterDrawSDFMask(painter, &image, (x >> 6) + face->glyph->bitmap_left, (y >> 6) - face->glyph->bitmap_top, 0x70);
} else {
mPainterDrawMask(painter, &image, (x >> 6) + face->glyph->bitmap_left, (y >> 6) - face->glyph->bitmap_top);
}
x += face->glyph->advance.x;
y += face->glyph->advance.y;
lastGlyph = codepoint;
}
return NULL;
}
void mPainterDrawText(struct mPainter* painter, const char* text, int x, int y, enum mAlignment alignment) {
FT_Face face = painter->font->face;
uint32_t lastGlyph = 0;
struct mSize size;
mFontTextBoxSize(painter->font, text, 0, &size);
x <<= 6;
y <<= 6;
@ -116,89 +206,33 @@ void mPainterDrawText(struct mPainter* painter, const char* text, int x, int y,
case mALIGN_BASELINE:
break;
case mALIGN_VCENTER:
y += face->size->metrics.ascender - face->size->metrics.height / 2;
y += face->size->metrics.ascender - size.height / 2;
break;
case mALIGN_BOTTOM:
default:
y += face->size->metrics.descender;
break;
}
switch (alignment & mALIGN_HORIZONTAL) {
case mALIGN_LEFT:
default:
break;
case mALIGN_HCENTER:
x -= mFontSpanWidth(painter->font, text) >> 1;
break;
case mALIGN_RIGHT:
x -= mFontSpanWidth(painter->font, text);
y += face->size->metrics.ascender - size.height;
break;
}
#if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 11
if (painter->strokeWidth) {
int xx = x;
int yy = y;
const char* ltext = text;
uint32_t fillColor = painter->fillColor;
painter->fillColor = painter->strokeColor;
while (*ltext) {
uint32_t glyph = utf8Char((const char**) &ltext, NULL);
if (FT_Load_Char(face, glyph, FT_LOAD_DEFAULT)) {
continue;
}
if (FT_Render_Glyph(face->glyph, FT_RENDER_MODE_SDF)) {
continue;
}
struct mImage image;
_makeTemporaryImage(&image, &face->glyph->bitmap);
FT_Vector kerning = {0};
FT_Get_Kerning(face, lastGlyph, glyph, FT_KERNING_DEFAULT, &kerning);
xx += kerning.x;
yy += kerning.y;
mPainterDrawSDFMask(painter, &image, (xx >> 6) + face->glyph->bitmap_left, (yy >> 6) - face->glyph->bitmap_top, 0x70);
xx += face->glyph->advance.x;
yy += face->glyph->advance.y;
lastGlyph = glyph;
}
do {
ltext = mPainterDrawTextRun(painter, ltext, x, yy, alignment, 0x70);
yy += face->size->metrics.height;
} while (ltext);
painter->fillColor = fillColor;
lastGlyph = 0;
}
#endif
while (*text) {
uint32_t glyph = utf8Char((const char**) &text, NULL);
if (FT_Load_Char(face, glyph, FT_LOAD_DEFAULT)) {
continue;
}
if (FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL)) {
continue;
}
struct mImage image;
_makeTemporaryImage(&image, &face->glyph->bitmap);
FT_Vector kerning = {0};
FT_Get_Kerning(face, lastGlyph, glyph, FT_KERNING_DEFAULT, &kerning);
x += kerning.x;
y += kerning.y;
mPainterDrawMask(painter, &image, (x >> 6) + face->glyph->bitmap_left, (y >> 6) - face->glyph->bitmap_top);
x += face->glyph->advance.x;
y += face->glyph->advance.y;
lastGlyph = glyph;
}
do {
text = mPainterDrawTextRun(painter, text, x, y, alignment, 0);
y += face->size->metrics.height;
} while (text);
}
#endif