Util: Add basic text rendering, expose to scripting

This commit is contained in:
Vicki Pfau 2025-05-31 22:54:40 -07:00
parent 8a4750f4fe
commit 67b5a51614
7 changed files with 276 additions and 1 deletions

View File

@ -60,6 +60,7 @@ if(NOT LIBMGBA_ONLY)
set(USE_ELF ON CACHE BOOL "Whether or not to enable ELF support")
set(USE_LUA ON CACHE BOOL "Whether or not to enable Lua scripting support")
set(USE_JSON_C ON CACHE BOOL "Whether or not to enable JSON-C support")
set(USE_FREETYPE ON CACHE BOOL "Whether or not to enable font rendering for scripts")
set(M_CORE_GBA ON CACHE BOOL "Build Game Boy Advance core")
set(M_CORE_GB ON CACHE BOOL "Build Game Boy core")
set(USE_LZMA ON CACHE BOOL "Whether or not to enable 7-Zip support")
@ -486,6 +487,7 @@ endif()
if(DISABLE_DEPS)
set(ENABLE_GDB_STUB OFF)
set(USE_DISCORD_RPC OFF)
set(USE_FREETYPE OFF)
set(USE_JSON_C OFF)
set(USE_SQLITE3 OFF)
set(USE_PNG OFF)
@ -514,6 +516,7 @@ find_feature(USE_EPOXY "epoxy")
find_feature(USE_CMOCKA "cmocka")
find_feature(USE_SQLITE3 "SQLite3|sqlite3")
find_feature(USE_ELF "libelf")
find_feature(USE_FREETYPE "Freetype")
find_feature(ENABLE_PYTHON "PythonLibs")
# Features
@ -763,6 +766,12 @@ if(USE_ELF)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libelf1")
endif()
if (USE_FREETYPE)
list(APPEND FEATURES FREETYPE)
list(APPEND DEPENDENCY_LIB Freetype::Freetype)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libfreetype6")
endif()
if (USE_DISCORD_RPC)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.7")
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/third-party/discord-rpc discord-rpc EXCLUDE_FROM_ALL)
@ -1343,6 +1352,7 @@ if(NOT QUIET AND NOT LIBMGBA_ONLY)
message(STATUS " Lua: ${USE_LUA}")
endif()
message(STATUS " storage API: ${USE_JSON_C}")
message(STATUS " Font rendering: ${USE_FREETYPE}")
endif()
message(STATUS "Frontends:")
message(STATUS " Qt: ${BUILD_QT}")

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2013-2015 Jeffrey Pfau
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@ -101,8 +101,10 @@ struct mImage {
enum mColorFormat format;
};
struct mFont;
struct mPainter {
struct mImage* backing;
struct mFont* font;
bool blend;
bool fill;
unsigned strokeWidth;
@ -110,6 +112,20 @@ struct mPainter {
uint32_t fillColor;
};
enum mAlignment {
mALIGN_LEFT = 0x01,
mALIGN_HCENTER = 0x02,
mALIGN_RIGHT = 0x03,
mALIGN_TOP = 0x10,
mALIGN_VCENTER = 0x20,
mALIGN_BOTTOM = 0x30,
mALIGN_BASELINE = 0x40,
mALIGN_HORIZONTAL = 0x03,
mALIGN_VERTICAL = 0x70,
};
struct VFile;
struct mImage* mImageCreate(unsigned width, unsigned height, enum mColorFormat format);
struct mImage* mImageCreateWithStride(unsigned width, unsigned height, unsigned stride, enum mColorFormat format);
@ -147,6 +163,17 @@ void mPainterDrawMask(struct mPainter*, const struct mImage* mask, int x, int y)
uint32_t mColorConvert(uint32_t color, enum mColorFormat from, enum mColorFormat to);
uint32_t mImageColorConvert(uint32_t color, const struct mImage* from, enum mColorFormat to);
#ifdef USE_FREETYPE
struct mFont* mFontOpen(const char* path);
void mFontDestroy(struct mFont*);
unsigned mFontSize(const struct mFont*);
void mFontSetSize(struct mFont*, unsigned px);
int mFontSpanWidth(struct mFont*, const char* text);
void mPainterDrawText(struct mPainter*, const char* text, int x, int y, enum mAlignment);
#endif
#ifndef PYCPARSE
static inline unsigned mColorFormatBytes(enum mColorFormat format) {
switch (format) {

View File

@ -99,6 +99,10 @@
#cmakedefine USE_FFMPEG
#endif
#ifndef USE_FREETYPE
#cmakedefine USE_FREETYPE
#endif
#ifndef USE_JSON_C
#cmakedefine USE_JSON_C
#endif

View File

@ -58,6 +58,10 @@
#endif
#endif
#ifdef USE_FREETYPE
#include <freetype/freetype.h>
#endif
#ifdef USE_LIBZIP
#include <zip.h>
#endif
@ -149,6 +153,11 @@ void ReportView::generateReport() {
#else
swReport << QString("FFmpeg not linked");
#endif
#ifdef USE_FREETYPE
swReport << QString("FreeType version: %1.%2.%3").arg(FREETYPE_MAJOR).arg(FREETYPE_MINOR).arg(FREETYPE_PATCH);
#else
swReport << QString("FreeType not linked");
#endif
#ifdef USE_EDITLINE
swReport << QString("libedit version: %1.%2").arg(LIBEDIT_MAJOR).arg(LIBEDIT_MINOR);
#else

View File

@ -124,6 +124,34 @@ void _mPainterSetStrokeColor(struct mPainter* painter, uint32_t color) {
painter->strokeColor = color;
}
#ifdef USE_FREETYPE
void _mPainterLoadFont(struct mPainter* painter, const char* path) {
struct mFont* font = mFontOpen(path);
if (!font) {
return;
}
if (painter->font) {
mFontDestroy(painter->font);
}
painter->font = font;
}
void _mPainterSetFontSize(struct mPainter* painter, float pt) {
if (!painter->font) {
return;
}
mFontSetSize(painter->font, pt * 64);
}
float _mPainterTextSpanWidth(struct mPainter* painter, const char* text) {
if (!painter->font) {
return 0;
}
return mFontSpanWidth(painter->font, text) / 64.f;
}
#endif
static struct mScriptValue* _mScriptPainterGet(struct mScriptPainter* painter, const char* name) {
struct mScriptValue val;
struct mScriptValue realPainter = mSCRIPT_MAKE(S(mPainter), &painter->painter);
@ -139,6 +167,11 @@ static struct mScriptValue* _mScriptPainterGet(struct mScriptPainter* painter, c
void _mScriptPainterDeinit(struct mScriptPainter* painter) {
mScriptValueDeref(painter->image);
#ifdef USE_FREETYPE
if (painter->painter.font) {
mFontDestroy(painter->painter.font);
}
#endif
free(painter);
}
@ -151,6 +184,12 @@ mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawRectangle, mPainterDrawRectangl
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mPainter, drawLine, mPainterDrawLine, 4, S32, x1, S32, y1, S32, x2, S32, y2);
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(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);
#endif
mSCRIPT_DEFINE_STRUCT(mPainter)
mSCRIPT_DEFINE_CLASS_DOCSTRING(
@ -179,6 +218,16 @@ mSCRIPT_DEFINE_STRUCT(mPainter)
"target color and use this function to draw it into a destination image."
)
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, drawMask)
#ifdef USE_FREETYPE
mSCRIPT_DEFINE_DOCSTRING("Draw text with the currently set font and fill color")
mSCRIPT_DEFINE_STRUCT_METHOD(mPainter, drawText)
mSCRIPT_DEFINE_DOCSTRING("Load a font from a given filename")
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)
#endif
mSCRIPT_DEFINE_END;
mSCRIPT_DECLARE_STRUCT_METHOD(mScriptPainter, W(mPainter), _get, _mScriptPainterGet, 1, CHARP, name);
@ -206,4 +255,15 @@ void mScriptContextAttachImage(struct mScriptContext* context) {
mScriptContextSetDocstring(context, "image.load", "Load an image from a path. Currently, only `PNG` format is supported");
#endif
mScriptContextSetDocstring(context, "image.newPainter", "Create a new painter from an existing image");
mScriptContextExportConstants(context, "ALIGN", (struct mScriptKVPair[]) {
mSCRIPT_CONSTANT_PAIR(mALIGN, LEFT),
mSCRIPT_CONSTANT_PAIR(mALIGN, HCENTER),
mSCRIPT_CONSTANT_PAIR(mALIGN, RIGHT),
mSCRIPT_CONSTANT_PAIR(mALIGN, TOP),
mSCRIPT_CONSTANT_PAIR(mALIGN, VCENTER),
mSCRIPT_CONSTANT_PAIR(mALIGN, BOTTOM),
mSCRIPT_CONSTANT_PAIR(mALIGN, BASELINE),
mSCRIPT_KV_SENTINEL
});
}

View File

@ -22,6 +22,7 @@ set(SOURCE_FILES
geometry.c
image.c
image/export.c
image/font.c
image/png-io.c
interpolator.c
patch.c

164
src/util/image/font.c Normal file
View File

@ -0,0 +1,164 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba-util/image.h>
#ifdef USE_FREETYPE
#include <mgba-util/string.h>
#include <freetype/freetype.h>
#define DPI 100
static _Atomic size_t libraryOpen = 0;
static FT_Library library;
struct mFont {
FT_Face face;
unsigned emHeight;
};
static void _makeTemporaryImage(struct mImage* out, FT_Bitmap* in) {
out->data = in->buffer;
out->width = in->width;
out->height = in->rows;
out->palette = NULL;
if (in->pixel_mode == FT_PIXEL_MODE_GRAY) {
out->stride = in->pitch;
out->format = mCOLOR_L8;
out->depth = 1;
} else {
abort();
}
}
struct mFont* mFontOpen(const char* path) {
size_t opened = ++libraryOpen;
if (opened == 1) {
if (FT_Init_FreeType(&library)) {
return NULL;
}
}
FT_Face face;
if (FT_New_Face(library, path, 0, &face)) {
return NULL;
}
struct mFont* font = calloc(1, sizeof(*font));
font->face = face;
mFontSetSize(font, 8 * 64);
return font;
}
void mFontDestroy(struct mFont* font) {
FT_Done_Face(font->face);
free(font);
size_t opened = --libraryOpen;
if (opened == 0) {
FT_Done_FreeType(library);
}
}
unsigned mFontSize(const struct mFont* font) {
return font->emHeight;
}
void mFontSetSize(struct mFont* font, unsigned pt) {
font->emHeight = pt;
FT_Set_Char_Size(font->face, 0, pt, DPI, DPI);
}
int mFontSpanWidth(struct mFont* font, const char* text) {
FT_Face face = font->face;
uint32_t lastGlyph = 0;
int width = 0;
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;
}
FT_Vector kerning = {0};
FT_Get_Kerning(face, lastGlyph, glyph, FT_KERNING_DEFAULT, &kerning);
width += kerning.x;
width += face->glyph->advance.x;
lastGlyph = glyph;
}
return width;
}
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;
x <<= 6;
y <<= 6;
switch (alignment & mALIGN_VERTICAL) {
case mALIGN_TOP:
y += face->size->metrics.ascender;
break;
case mALIGN_BASELINE:
break;
case mALIGN_VCENTER:
y += face->size->metrics.ascender - face->size->metrics.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);
break;
}
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;
}
}
#endif