diff --git a/include/mgba-util/image.h b/include/mgba-util/image.h index 5558910dc..00af9148c 100644 --- a/include/mgba-util/image.h +++ b/include/mgba-util/image.h @@ -10,6 +10,8 @@ CXX_GUARD_START +#include + #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 diff --git a/res/scripts/demos/text-demo.lua b/res/scripts/demos/text-demo.lua index 070d2fa4d..7875e94c4 100644 --- a/res/scripts/demos/text-demo.lua +++ b/res/scripts/demos/text-demo.lua @@ -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() diff --git a/res/scripts/demos/text-metrics.lua b/res/scripts/demos/text-metrics.lua new file mode 100644 index 000000000..5334ba2f6 --- /dev/null +++ b/res/scripts/demos/text-metrics.lua @@ -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() diff --git a/src/script/image.c b/src/script/image.c index 411a95c45..e65e207ee 100644 --- a/src/script/image.c +++ b/src/script/image.c @@ -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), diff --git a/src/util/image/font.c b/src/util/image/font.c index b4e191726..a7689b355 100644 --- a/src/util/image/font.c +++ b/src/util/image/font.c @@ -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**) <ext, 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