From 2ad8c60abcd4eb6b6b71003936fde126463ebc5e Mon Sep 17 00:00:00 2001 From: sakiodre <136492105+sakiodre@users.noreply.github.com> Date: Tue, 1 Aug 2023 09:27:08 +0200 Subject: [PATCH] Fonts: Added support for OpenType SVG fonts using lunasvg (#6591, #6607) --- imconfig.h | 8 ++ misc/freetype/README.md | 8 ++ misc/freetype/imgui_freetype.cpp | 157 +++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/imconfig.h b/imconfig.h index 886811338..fb4d551f1 100644 --- a/imconfig.h +++ b/imconfig.h @@ -80,6 +80,14 @@ // The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. //#define IMGUI_ENABLE_STB_TRUETYPE +//---- Use lunasvg library to render OpenType SVG fonts (SVGinOT) +// Requires lunasvg headers to be available in the include path. Requires program to be compiled with the lunasvg library (not provided). +// See https://github.com/ocornut/imgui/tree/master/misc/freetype +// Only works in combination with IMGUI_ENABLE_FREETYPE. +// The implementation is based on the demo https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c +// which is licensed under CeCILL-C Free Software License Agreement +//#define IMGUI_ENABLE_FREETYPE_LUNASVG + //---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. // This will be inlined as part of ImVec2 and ImVec4 class declarations. /* diff --git a/misc/freetype/README.md b/misc/freetype/README.md index b25b85d5a..2f4e4db3c 100644 --- a/misc/freetype/README.md +++ b/misc/freetype/README.md @@ -35,3 +35,11 @@ You can use the `ImGuiFreeTypeBuilderFlags_LoadColor` flag to load certain color ["Using Colorful Glyphs/Emojis"](https://github.com/ocornut/imgui/blob/master/docs/FONTS.md#using-colorful-glyphsemojis) section of FONTS.md. ![colored glyphs](https://user-images.githubusercontent.com/8225057/106171241-9dc4ba80-6191-11eb-8a69-ca1467b206d1.png) + + +### Using OpenType SVG fonts (SVGinOT) +- *SVG in Open Type* is a standard by Adobe and Mozilla for color OpenType and Open Font Format fonts. It allows font creators to embed complete SVG files within a font enabling full color and even animations. +- Popular fonts such as [twemoji](https://github.com/13rac1/twemoji-color-font) and fonts made with [scfbuild](https://github.com/13rac1/scfbuild) is SVGinOT +- Requires: [lunasvg](https://github.com/sammycage/lunasvg) v2.3.2 and above + 1. Add `#define IMGUI_ENABLE_FREETYPE_OTSVG` in your `imconfig.h`. + 2. Get latest lunasvg binaries or build yourself. Under Windows you may use vcpkg with: `vcpkg install lunasvg --triplet=x64-windows`. \ No newline at end of file diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index f1f6b8c5b..2686a54ff 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -6,6 +6,7 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2023/07/12: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG' // 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly. // 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns NULL. // 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs. @@ -44,6 +45,13 @@ #include FT_GLYPH_H // #include FT_SYNTHESIS_H // +#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG +#include FT_OTSVG_H // +#include FT_BBOX_H // +#include +#include +#endif + #ifdef _MSC_VER #pragma warning (push) #pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) @@ -70,6 +78,14 @@ static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFre static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc; static void* GImGuiFreeTypeAllocatorUserData = nullptr; + +#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG +FT_Error ImGuiLunasvgPortInit(FT_Pointer* state); +void ImGuiLunasvgPortFree(FT_Pointer* state); +FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state); +FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state); +#endif + //------------------------------------------------------------------------- // Code //------------------------------------------------------------------------- @@ -244,7 +260,19 @@ namespace // Need an outline for this to work FT_GlyphSlot slot = Face->glyph; + +#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12)) +#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG + IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG); +#else + IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && + "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_LUNASVG" + "in imconfig.h and install required libraries in order to use this font"); IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP); +#endif // IMGUI_ENABLE_FREETYPE_LUNASVG +#else + IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP); +#endif // ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12)) // Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting) if (UserFlags & ImGuiFreeTypeBuilderFlags_Bold) @@ -770,6 +798,18 @@ static bool ImFontAtlasBuildWithFreeType(ImFontAtlas* atlas) // If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator. FT_Add_Default_Modules(ft_library); +#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG +#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12)) + // Install svg hooks for FreeType + // https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks + // https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts + SVG_RendererHooks hooks = {ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot}; + FT_Property_Set(ft_library, "ot-svg", "svg-hooks", &hooks); +#else + IM_ASSERT(!"IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12"); +#endif // ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12)) +#endif // IMGUI_ENABLE_FREETYPE_LUNASVG + bool ret = ImFontAtlasBuildWithFreeTypeEx(ft_library, atlas, atlas->FontBuilderFlags); FT_Done_Library(ft_library); @@ -790,6 +830,123 @@ void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* u GImGuiFreeTypeAllocatorUserData = user_data; } +#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG +// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c +// The original code from the demo is licensed under CeCILL-C Free Software License Agreement +// https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT +typedef struct LunasvgPortState_ +{ + FT_Error err = FT_Err_Ok; + std::unique_ptr svg = nullptr; + lunasvg::Matrix matrix; // Scaled and translated matrix for rendering +} LunasvgPortState, *PLunasvgPortState; + +FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state) +{ + *_state = new LunasvgPortState(); + return FT_Err_Ok; +} + +void ImGuiLunasvgPortFree(FT_Pointer* _state) +{ + delete *_state; +} + +FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state) +{ + PLunasvgPortState state = *(PLunasvgPortState*)_state; + + // If there was an error while loading the svg in + // `ImGuiLunasvgPortPresetSlot`, the renderer hook + // still get called, so just returns the error. + if (state->err != FT_Err_Ok) return state->err; + + // rows is height, pitch (or stride) equals to width * sizeof(int32) + lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch); + + state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value + state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated + + state->err = FT_Err_Ok; + return state->err; +} + +FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state) +{ + FT_SVG_Document document = (FT_SVG_Document)slot->other; + PLunasvgPortState state = *(PLunasvgPortState*)_state; + FT_Size_Metrics& metrics = document->metrics; + + // This function is called twice, once in the `FT_Load_Glyph` + // and another right before `ImGuiLunasvgPortRender`. + // If it's the latter, don't do anything because it's + // already done in the former. + if (cache) return state->err; + + state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length); + if (state->svg == nullptr) + { + state->err = FT_Err_Invalid_SVG_Document; + return state->err; + } + + lunasvg::Box box = state->svg->box(); + + double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h); + double xx = (double)document->transform.xx / (1 << 16); + double xy = -(double)document->transform.xy / (1 << 16); + double yx = -(double)document->transform.yx / (1 << 16); + double yy = (double)document->transform.yy / (1 << 16); + double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem; + double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem; + + // This reset the matrix to its default value + state->matrix.identity(); + + // Scale and transform, we don't translate the svg yet + state->matrix.scale(scale, scale); + state->matrix.transform(xx, xy, yx, yy, x0, y0); + state->svg->setMatrix(state->matrix); + + // Pre-translate the matrix for the rendering step + state->matrix.translate(-box.x, -box.y); + + // Get the box again after the transformation + box = state->svg->box(); + + // Calculate the bitmap size + slot->bitmap_left = FT_Int(box.x); + slot->bitmap_top = FT_Int(-box.y); + slot->bitmap.rows = (unsigned int)(ceil(box.h)); + slot->bitmap.width = (unsigned int)(ceil(box.w)); + slot->bitmap.pitch = slot->bitmap.width * 4; + slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + + // Compute all the bearings and set them correctly. The outline is + // scaled already, we just need to use the bounding box. + double metrics_width = box.w; + double metrics_height = box.h; + double horiBearingX = box.x; + double horiBearingY = -box.y; + + double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0; + double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0; + + slot->metrics.width = FT_Pos(round(metrics_width * 64.0)); + slot->metrics.height = FT_Pos(round(metrics_height * 64.0)); + + slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64); + slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64); + slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64); + slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64); + + if (slot->metrics.vertAdvance == 0) + slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0); + + state->err = FT_Err_Ok; + return state->err; +} +#endif // !IMGUI_ENABLE_FREETYPE_LUNASVG //----------------------------------------------------------------------------- #ifdef __GNUC__