diff --git a/CMakeLists.txt b/CMakeLists.txt index f7e84cf0d..fbe40665e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}") diff --git a/include/mgba-util/image.h b/include/mgba-util/image.h index dd135a0b4..b302ef239 100644 --- a/include/mgba-util/image.h +++ b/include/mgba-util/image.h @@ -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) { diff --git a/src/core/flags.h.in b/src/core/flags.h.in index 1071d42c3..e820c9d0c 100644 --- a/src/core/flags.h.in +++ b/src/core/flags.h.in @@ -99,6 +99,10 @@ #cmakedefine USE_FFMPEG #endif +#ifndef USE_FREETYPE +#cmakedefine USE_FREETYPE +#endif + #ifndef USE_JSON_C #cmakedefine USE_JSON_C #endif diff --git a/src/platform/qt/ReportView.cpp b/src/platform/qt/ReportView.cpp index 2aca47663..18a951783 100644 --- a/src/platform/qt/ReportView.cpp +++ b/src/platform/qt/ReportView.cpp @@ -58,6 +58,10 @@ #endif #endif +#ifdef USE_FREETYPE +#include +#endif + #ifdef USE_LIBZIP #include #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 diff --git a/src/script/image.c b/src/script/image.c index 533af131f..411a95c45 100644 --- a/src/script/image.c +++ b/src/script/image.c @@ -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 + }); } diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 8009f5ad2..6dd6ae2a4 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -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 diff --git a/src/util/image/font.c b/src/util/image/font.c new file mode 100644 index 000000000..3b195fa5b --- /dev/null +++ b/src/util/image/font.c @@ -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 + +#ifdef USE_FREETYPE + +#include + +#include + +#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