From f8e941e72fd068ef011ec52eabb246f6a25b1c37 Mon Sep 17 00:00:00 2001 From: Green Sky Date: Wed, 22 Oct 2025 21:52:07 +0200 Subject: [PATCH 1/5] Make FreeType select a size for fixed sized fonts and scale it down before adding it to atlas to save on atlas space. --- misc/freetype/imgui_freetype.cpp | 125 +++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 22 deletions(-) diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index cbbde767c..d75cc6818 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -108,7 +108,7 @@ static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_ //------------------------------------------------------------------------- #define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point -#define FT_SCALEFACTOR 64.0f +#define FT_SCALEFACTOR 64.0f // For converting from/to 26.6 factionals // Glyph metrics: // -------------- @@ -435,19 +435,58 @@ static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* s FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize); FT_Activate_Size(bd_baked_data->FtSize); - - // Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height' - // is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me. - // FT_Set_Pixel_Sizes() doesn't seem to get us the same result." - // (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL) const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity; - FT_Size_RequestRec req; - req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM; - req.width = 0; - req.height = (uint32_t)(size * 64 * rasterizer_density); - req.horiResolution = 0; - req.vertResolution = 0; - FT_Request_Size(bd_font_data->FtFace, &req); + if (((bd_font_data->FtFace->face_flags & FT_FACE_FLAG_FIXED_SIZES) != 0) && ((bd_font_data->FtFace->face_flags & FT_FACE_FLAG_SCALABLE) == 0) && ((bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) != 0)) + { + IM_ASSERT(bd_font_data->FtFace->num_fixed_sizes > 0); + + // Loop over sizes and pick the closest, larger (or better equal) size. + int best_index = 0; + float best_height = bd_font_data->FtFace->available_sizes[best_index].y_ppem / FT_SCALEFACTOR; + for (int i = 1; i < bd_font_data->FtFace->num_fixed_sizes; i++) + { + const float cur_height = bd_font_data->FtFace->available_sizes[i].y_ppem / FT_SCALEFACTOR; + // TODO: is this overkill? + // TODO: is-float-close with epsilon param would be nice, maybe + if (ImFabs(cur_height - size) < 0.001f) + { + best_index = i; + break; + } + else if (cur_height < size) + { + if (best_height < cur_height) + { + best_index = i; + best_height = cur_height; + } + } + else + { + if (best_height > cur_height) + { + best_index = i; + best_height = cur_height; + } + } + } + FT_Select_Size(bd_font_data->FtFace, best_index); + } + else + { + // Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height' + // is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me. + // FT_Set_Pixel_Sizes() doesn't seem to get us the same result." + // (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL) + + FT_Size_RequestRec req; + req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM; + req.width = 0; + req.height = (uint32_t)(size * FT_SCALEFACTOR * rasterizer_density); + req.horiResolution = 0; + req.vertResolution = 0; + FT_Request_Size(bd_font_data->FtFace, &req); + } // Output if (src->MergeMode == false) @@ -497,9 +536,21 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf FT_Face face = bd_font_data->FtFace; FT_GlyphSlot slot = face->glyph; const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity; + float bitmap_x_scale = 1.f; + float bitmap_y_scale = 1.f; + if (((face->face_flags & FT_FACE_FLAG_FIXED_SIZES) != 0) && ((face->face_flags & FT_FACE_FLAG_SCALABLE) == 0) && ((bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) != 0)) + { + // TODO: what if this just means invisible? + IM_ASSERT(bd_font_data->FtFace->size->metrics.x_ppem > 0); + IM_ASSERT(bd_font_data->FtFace->size->metrics.y_ppem > 0); + + // Scale fixed size bitmap to target size + bitmap_x_scale = baked->Size / bd_font_data->FtFace->size->metrics.x_ppem; + bitmap_y_scale = baked->Size / bd_font_data->FtFace->size->metrics.y_ppem; + } // Load metrics only mode - const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density; + const float advance_x = ((slot->advance.x / FT_SCALEFACTOR) * bitmap_x_scale) / rasterizer_density; if (out_advance_x != NULL) { IM_ASSERT(out_glyph == NULL); @@ -514,9 +565,9 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf if (error != 0 || ft_bitmap == nullptr) return false; - const int w = (int)ft_bitmap->width; - const int h = (int)ft_bitmap->rows; - const bool is_visible = (w != 0 && h != 0); + const int bitmap_w = (int)ft_bitmap->width; + const int bitmap_h = (int)ft_bitmap->rows; + const bool is_visible = (bitmap_w != 0 && bitmap_h != 0); // Prepare glyph out_glyph->Codepoint = codepoint; @@ -525,6 +576,11 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf // Pack and retrieve position inside texture atlas if (is_visible) { + const int w = (int)ImFloor(bitmap_w * bitmap_x_scale); + const int h = (int)ImFloor(bitmap_h * bitmap_y_scale); + IM_ASSERT(!((h < bitmap_h && w > bitmap_w) || (h > bitmap_h && w < bitmap_w))); // Can't up AND downscale at the same time // TODO: or can we? + const bool down_scaling = h < bitmap_h || w < bitmap_w; + ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h); if (pack_id == ImFontAtlasRectId_Invalid) { @@ -534,10 +590,35 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf } ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id); - // Render pixels to our temporary buffer - atlas->Builder->TempBuffer.resize(w * h * 4); + // Render pixels to our temporary buffer, while making sure we have space for an extra copy used during downscaling. + atlas->Builder->TempBuffer.resize(((down_scaling ? bitmap_w * bitmap_h : 0) + w * h) * 4); uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data; - ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w); + // Blit (and convert) into the first bm_w * bm_h * 4 bytes. + ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, bitmap_w); + + if (down_scaling) + { + uint32_t* dst_buffer = temp_buffer + bitmap_w * bitmap_h; + // Perform downscale, from temp_buffer (bitmap_w * bitmap_h) to dst_buffer (w * h) + +#if 1 + // Point Sampling / Nearest Neighbor + for (int y = 0; y < h; y++) + { + const int bitmap_y = ImFloor(((y + 0.5f) * bitmap_h) / h); + for (int x = 0; x < w; x++) + { + const int bitmap_x = ImFloor(((x + 0.5f) * bitmap_w) / w); + dst_buffer[y * w + x] = temp_buffer[bitmap_y * bitmap_w + bitmap_x]; + } + } +#else + // TODO: box scaling +#endif + + // Redirect to downscaled part of the buffer + temp_buffer = dst_buffer; + } const float ref_size = baked->ContainerFont->Sources[0]->SizePixels; const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f; @@ -551,8 +632,8 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf float recip_v = 1.0f / rasterizer_density; // Register glyph - float glyph_off_x = (float)face->glyph->bitmap_left; - float glyph_off_y = (float)-face->glyph->bitmap_top; + float glyph_off_x = ImFloor((float)face->glyph->bitmap_left * bitmap_x_scale); + float glyph_off_y = ImFloor((float)-face->glyph->bitmap_top * bitmap_y_scale); out_glyph->X0 = glyph_off_x * recip_h + font_off_x; out_glyph->Y0 = glyph_off_y * recip_v + font_off_y; out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x; From 85d3a874a4c9314f0a120a0a4ff84f1fc38e0748 Mon Sep 17 00:00:00 2001 From: Green Sky Date: Fri, 24 Oct 2025 17:39:13 +0200 Subject: [PATCH 2/5] Ceil rather than Floor. Looks more correct and more in line with other parts of the code. --- misc/freetype/imgui_freetype.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index d75cc6818..0a97caddd 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -576,8 +576,8 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf // Pack and retrieve position inside texture atlas if (is_visible) { - const int w = (int)ImFloor(bitmap_w * bitmap_x_scale); - const int h = (int)ImFloor(bitmap_h * bitmap_y_scale); + const int w = (int)ImCeil(bitmap_w * bitmap_x_scale); + const int h = (int)ImCeil(bitmap_h * bitmap_y_scale); IM_ASSERT(!((h < bitmap_h && w > bitmap_w) || (h > bitmap_h && w < bitmap_w))); // Can't up AND downscale at the same time // TODO: or can we? const bool down_scaling = h < bitmap_h || w < bitmap_w; @@ -632,8 +632,8 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf float recip_v = 1.0f / rasterizer_density; // Register glyph - float glyph_off_x = ImFloor((float)face->glyph->bitmap_left * bitmap_x_scale); - float glyph_off_y = ImFloor((float)-face->glyph->bitmap_top * bitmap_y_scale); + float glyph_off_x = ImCeil((float)face->glyph->bitmap_left * bitmap_x_scale); + float glyph_off_y = ImCeil((float)-face->glyph->bitmap_top * bitmap_y_scale); out_glyph->X0 = glyph_off_x * recip_h + font_off_x; out_glyph->Y0 = glyph_off_y * recip_v + font_off_y; out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x; From 9530ad50b5e811fdd11f770c422138b936042528 Mon Sep 17 00:00:00 2001 From: Green Sky Date: Fri, 24 Oct 2025 22:34:04 +0200 Subject: [PATCH 3/5] implement box sampling downscaler --- misc/freetype/imgui_freetype.cpp | 83 ++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index 0a97caddd..7c0edfc44 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -514,6 +514,72 @@ static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE() } +static void DownscaleBitmap(uint32_t* dst, const int dst_w, const int dst_h, uint32_t* src, const int src_w, const int src_h) { + IM_ASSERT(dst_w <= src_w && dst_h <= src_h); // TODO: check if this is required + +#if 0 + // Point Sampling / Nearest Neighbor + for (int y = 0; y < dst_h; y++) + { + const int src_y = ImFloor(((y + 0.5f) * src_h) / dst_h); + for (int x = 0; x < dst_w; x++) + { + const int src_x = ImFloor(((x + 0.5f) * src_w) / dst_w); + dst[y * dst_w + x] = src[src_y * src_w + src_x]; + } + } +#else + // Box sampling - Imagine projecting the new, smaller pixels onto the larger source, covering multiple pixel. + for (int y = 0; y < dst_h; y++) + { + for (int x = 0; x < dst_w; x++) + { + // We perform a weighted mean. + ImVec4 color; + float weight_sum = 0.f; + + // Walk from upper edge to bottom edge (vertical) + const float edge_up = ((float)y * src_h) / dst_h; + const float edge_down = ((y + 1.f) * src_h) / dst_h; + for (float frac_pos_y = edge_up; frac_pos_y < edge_down;) + { + const int src_y = (int)ImFloor(frac_pos_y); IM_ASSERT(src_y < src_h); + const float frac_y = 1.f - (frac_pos_y - src_y); + + // Walk from left edge to right edge (horizontal) + const float edge_left = ((float)x * src_w) / dst_w; + const float edge_right = ((x + 1.f) * src_w) / dst_w; + for (float frac_pos_x = edge_left; frac_pos_x < edge_right;) + { + const int src_x = (int)ImFloor(frac_pos_x); IM_ASSERT(src_x < src_w); + const float frac_x = 1.f - (frac_pos_x - src_x); + + const float src_pixel_weight = frac_x * frac_y; + + const ImVec4 pixel_color = ImGui::ColorConvertU32ToFloat4(src[src_y * src_w + src_x]); + color.x += pixel_color.x * src_pixel_weight; + color.y += pixel_color.y * src_pixel_weight; + color.z += pixel_color.z * src_pixel_weight; + color.w += pixel_color.w * src_pixel_weight; + weight_sum += src_pixel_weight; + + frac_pos_x += frac_x; + } + + frac_pos_y += frac_y; + } + + color.x /= weight_sum; + color.y /= weight_sum; + color.z /= weight_sum; + color.w /= weight_sum; + + dst[y * dst_w + x] = ImGui::ColorConvertFloat4ToU32(color); + } + } +#endif +} + static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x) { ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData; @@ -599,22 +665,9 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf if (down_scaling) { uint32_t* dst_buffer = temp_buffer + bitmap_w * bitmap_h; - // Perform downscale, from temp_buffer (bitmap_w * bitmap_h) to dst_buffer (w * h) -#if 1 - // Point Sampling / Nearest Neighbor - for (int y = 0; y < h; y++) - { - const int bitmap_y = ImFloor(((y + 0.5f) * bitmap_h) / h); - for (int x = 0; x < w; x++) - { - const int bitmap_x = ImFloor(((x + 0.5f) * bitmap_w) / w); - dst_buffer[y * w + x] = temp_buffer[bitmap_y * bitmap_w + bitmap_x]; - } - } -#else - // TODO: box scaling -#endif + // Perform downscale, from temp_buffer (bitmap_w * bitmap_h) to dst_buffer (w * h) + DownscaleBitmap(dst_buffer, w, h, temp_buffer, bitmap_w, bitmap_h); // Redirect to downscaled part of the buffer temp_buffer = dst_buffer; From d5d23835e2ef9952b43224fb39946fdc194284cd Mon Sep 17 00:00:00 2001 From: Green Sky Date: Sun, 26 Oct 2025 11:03:48 +0100 Subject: [PATCH 4/5] always rescale when bitmap != glyph size --- misc/freetype/imgui_freetype.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index 7c0edfc44..43cc8a334 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -514,9 +514,8 @@ static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE() } -static void DownscaleBitmap(uint32_t* dst, const int dst_w, const int dst_h, uint32_t* src, const int src_w, const int src_h) { - IM_ASSERT(dst_w <= src_w && dst_h <= src_h); // TODO: check if this is required - +static void RescaleBitmap(uint32_t* dst, const int dst_w, const int dst_h, uint32_t* src, const int src_w, const int src_h) +{ #if 0 // Point Sampling / Nearest Neighbor for (int y = 0; y < dst_h; y++) @@ -644,8 +643,7 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf { const int w = (int)ImCeil(bitmap_w * bitmap_x_scale); const int h = (int)ImCeil(bitmap_h * bitmap_y_scale); - IM_ASSERT(!((h < bitmap_h && w > bitmap_w) || (h > bitmap_h && w < bitmap_w))); // Can't up AND downscale at the same time // TODO: or can we? - const bool down_scaling = h < bitmap_h || w < bitmap_w; + const bool rescaling = h != bitmap_h || w != bitmap_w; ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h); if (pack_id == ImFontAtlasRectId_Invalid) @@ -656,20 +654,20 @@ static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConf } ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id); - // Render pixels to our temporary buffer, while making sure we have space for an extra copy used during downscaling. - atlas->Builder->TempBuffer.resize(((down_scaling ? bitmap_w * bitmap_h : 0) + w * h) * 4); + // Render pixels to our temporary buffer, while making sure we have space for an extra copy used during rescaling. + atlas->Builder->TempBuffer.resize(((rescaling ? bitmap_w * bitmap_h : 0) + w * h) * 4); uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data; - // Blit (and convert) into the first bm_w * bm_h * 4 bytes. + // Blit (and convert) into the first bitmap_w * bitmap_h * 4 bytes. ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, bitmap_w); - if (down_scaling) + if (rescaling) { uint32_t* dst_buffer = temp_buffer + bitmap_w * bitmap_h; - // Perform downscale, from temp_buffer (bitmap_w * bitmap_h) to dst_buffer (w * h) - DownscaleBitmap(dst_buffer, w, h, temp_buffer, bitmap_w, bitmap_h); + // Perform rescale, from temp_buffer (bitmap_w * bitmap_h) to dst_buffer (w * h) + RescaleBitmap(dst_buffer, w, h, temp_buffer, bitmap_w, bitmap_h); - // Redirect to downscaled part of the buffer + // Redirect to rescaled part of the buffer temp_buffer = dst_buffer; } From 08fa252c96e2a34a4c97c1cae691cf20176e9599 Mon Sep 17 00:00:00 2001 From: Green Sky Date: Sun, 26 Oct 2025 11:04:27 +0100 Subject: [PATCH 5/5] correctly select the next larger bitmap size --- misc/freetype/imgui_freetype.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp index 43cc8a334..960f949a4 100644 --- a/misc/freetype/imgui_freetype.cpp +++ b/misc/freetype/imgui_freetype.cpp @@ -451,6 +451,7 @@ static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* s if (ImFabs(cur_height - size) < 0.001f) { best_index = i; + best_height = cur_height; break; } else if (cur_height < size) @@ -463,7 +464,7 @@ static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* s } else { - if (best_height > cur_height) + if (best_height < size && best_height < cur_height) { best_index = i; best_height = cur_height;