mirror of
https://github.com/Drezil/imgui.git
synced 2024-11-26 13:37:00 +00:00
ImFontAtlas: Rewrote stb_truetype based builder.
- Atlas width is now properly based on total surface rather than glyph count (unless overridden with TexDesiredWidth). - Fixed atlas builder so missing glyphs won't influence the atlas texture width. (#2233) - Fixed atlas builder so duplicate glyphs (when merging fonts) won't be included in the rasterized atlas.
This commit is contained in:
parent
e3ccc96789
commit
9a9712807e
@ -72,6 +72,9 @@ Other Changes:
|
|||||||
Missing calls to End(), past the assert, should not lead to crashes or to the fallback Debug window appearing on screen.
|
Missing calls to End(), past the assert, should not lead to crashes or to the fallback Debug window appearing on screen.
|
||||||
Those changes makes it easier to integrate dear imgui with a scripting language allowing, given asserts are redirected
|
Those changes makes it easier to integrate dear imgui with a scripting language allowing, given asserts are redirected
|
||||||
into e.g. an error log and stopping the script execution.
|
into e.g. an error log and stopping the script execution.
|
||||||
|
- ImFontAtlas: Stb: Atlas width is now properly based on total surface rather than glyph count (unless overridden with TexDesiredWidth).
|
||||||
|
- ImFontAtlas: Stb: Fixed atlas builder so missing glyphs won't influence the atlas texture width. (#2233)
|
||||||
|
- ImFontAtlas: Stb: Fixed atlas builder so duplicate glyphs (when merging fonts) won't be included in the rasterized atlas.
|
||||||
- ImDrawList: Optimized some of the functions for performance of debug builds where non-inline function call cost are non-negligible.
|
- ImDrawList: Optimized some of the functions for performance of debug builds where non-inline function call cost are non-negligible.
|
||||||
(Our test UI scene on VS2015 Debug Win64 with /RTC1 went ~5.9 ms -> ~4.9 ms. In Release same scene stays at ~0.3 ms.)
|
(Our test UI scene on VS2015 Debug Win64 with /RTC1 went ~5.9 ms -> ~4.9 ms. In Release same scene stays at ~0.3 ms.)
|
||||||
- IO: Added BackendPlatformUserData, BackendRendererUserData, BackendLanguageUserData void* for storage use by back-ends.
|
- IO: Added BackendPlatformUserData, BackendRendererUserData, BackendLanguageUserData void* for storage use by back-ends.
|
||||||
|
@ -235,9 +235,7 @@ It's mostly a bunch of personal notes, probably incomplete. Feel free to query i
|
|||||||
- pie menus patterns (#434)
|
- pie menus patterns (#434)
|
||||||
- markup: simple markup language for color change? (#902)
|
- markup: simple markup language for color change? (#902)
|
||||||
|
|
||||||
!- font: need handling of missing glyphs by not packing/rasterizing glyph 0 of a given font.
|
- font: MergeMode: flags to select overwriting or not (this is now very easy with refactored ImFontAtlasBuildWithStbTruetype)
|
||||||
- font: MergeMode: flags to select overwriting or not.
|
|
||||||
- font: MergeMode: duplicate glyphs are stored in the atlas texture which is suboptimal.
|
|
||||||
- font: free the Alpha buffer if user only requested RGBA.
|
- font: free the Alpha buffer if user only requested RGBA.
|
||||||
!- font: better CalcTextSizeA() API, at least for simple use cases. current one is horrible (perhaps have simple vs extended versions).
|
!- font: better CalcTextSizeA() API, at least for simple use cases. current one is horrible (perhaps have simple vs extended versions).
|
||||||
- font: a CalcTextHeight() helper could run faster than CalcTextSize().y
|
- font: a CalcTextHeight() helper could run faster than CalcTextSize().y
|
||||||
@ -252,7 +250,6 @@ It's mostly a bunch of personal notes, probably incomplete. Feel free to query i
|
|||||||
- font/draw: vertical and/or rotated text renderer (#705) - vertical is easier clipping wise
|
- font/draw: vertical and/or rotated text renderer (#705) - vertical is easier clipping wise
|
||||||
- font/draw: need to be able to specify wrap start position.
|
- font/draw: need to be able to specify wrap start position.
|
||||||
- font/draw: better reserve policy for large horizontal block of text (shouldn't reserve for all clipped lines)
|
- font/draw: better reserve policy for large horizontal block of text (shouldn't reserve for all clipped lines)
|
||||||
- font: imgui_freetype.h alternative renderer (#618)
|
|
||||||
- font: optimization: for monospace font (like the default one) we can trim IndexXAdvance as long as trailing value is == FallbackXAdvance (need to make sure TAB is still correct).
|
- font: optimization: for monospace font (like the default one) we can trim IndexXAdvance as long as trailing value is == FallbackXAdvance (need to make sure TAB is still correct).
|
||||||
- font: add support for kerning, probably optional. A) perhaps default to (32..128)^2 matrix ~ 9K entries = 36KB, then hash for non-ascii?. B) or sparse lookup into per-char list?
|
- font: add support for kerning, probably optional. A) perhaps default to (32..128)^2 matrix ~ 9K entries = 36KB, then hash for non-ascii?. B) or sparse lookup into per-char list?
|
||||||
- font: add a simpler CalcTextSizeA() api? current one ok but not welcome if user needs to call it directly (without going through ImGui::CalcTextSize)
|
- font: add a simpler CalcTextSizeA() api? current one ok but not welcome if user needs to call it directly (without going through ImGui::CalcTextSize)
|
||||||
|
2
imgui.h
2
imgui.h
@ -2047,7 +2047,7 @@ struct ImFontAtlas
|
|||||||
ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_)
|
ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_)
|
||||||
ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure.
|
ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure.
|
||||||
int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height.
|
int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height.
|
||||||
int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1.
|
int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0.
|
||||||
|
|
||||||
// [Internal]
|
// [Internal]
|
||||||
// NB: Access texture data via GetTexData*() calls! Which will setup a default font for you.
|
// NB: Access texture data via GetTexData*() calls! Which will setup a default font for you.
|
||||||
|
374
imgui_draw.cpp
374
imgui_draw.cpp
@ -1541,11 +1541,11 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg)
|
|||||||
if (!font_cfg->MergeMode)
|
if (!font_cfg->MergeMode)
|
||||||
Fonts.push_back(IM_NEW(ImFont));
|
Fonts.push_back(IM_NEW(ImFont));
|
||||||
else
|
else
|
||||||
IM_ASSERT(!Fonts.empty()); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font.
|
IM_ASSERT(!Fonts.empty() && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font.
|
||||||
|
|
||||||
ConfigData.push_back(*font_cfg);
|
ConfigData.push_back(*font_cfg);
|
||||||
ImFontConfig& new_font_cfg = ConfigData.back();
|
ImFontConfig& new_font_cfg = ConfigData.back();
|
||||||
if (!new_font_cfg.DstFont)
|
if (new_font_cfg.DstFont == NULL)
|
||||||
new_font_cfg.DstFont = Fonts.back();
|
new_font_cfg.DstFont = Fonts.back();
|
||||||
if (!new_font_cfg.FontDataOwnedByAtlas)
|
if (!new_font_cfg.FontDataOwnedByAtlas)
|
||||||
{
|
{
|
||||||
@ -1733,139 +1733,220 @@ void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsig
|
|||||||
data[i] = table[data[i]];
|
data[i] = table[data[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temporary data for one source font (multiple source fonts can be merged into one destination ImFont)
|
||||||
|
// (C++03 doesn't allow instancing ImVector<> with function-local types so we declare the type here.)
|
||||||
|
struct ImFontBuildSrcData
|
||||||
|
{
|
||||||
|
stbtt_fontinfo FontInfo;
|
||||||
|
stbtt_pack_range PackRange; // Hold the list of codepoints to pack (essentially points to Codepoints.Data)
|
||||||
|
stbrp_rect* Rects; // Rectangle to pack. We first fill in their size and the packer will give us their position.
|
||||||
|
stbtt_packedchar* PackedChars; // Output glyphs
|
||||||
|
const ImWchar* SrcRanges; // Ranges as requested by user (user is allowed to request too much, e.g. 0x0020..0xFFFF)
|
||||||
|
int DstIndex; // Index into atlas->Fonts[] and dst_tmp_array[]
|
||||||
|
int GlyphsHighest; // Highest requested codepoint
|
||||||
|
int GlyphsCount; // Glyph count (excluding missing glyphs and glyphs already set by an earlier source font)
|
||||||
|
ImBoolVector GlyphsSet; // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB)
|
||||||
|
ImVector<int> GlyphsList; // Glyph codepoints list (flattened version of GlyphsMap)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporary data for one destination ImFont* (multiple source fonts can be merged into one destination ImFont)
|
||||||
|
struct ImFontBuildDstData
|
||||||
|
{
|
||||||
|
int SrcCount; // Number of source fonts targeting this destination font.
|
||||||
|
int GlyphsHighest;
|
||||||
|
int GlyphsCount;
|
||||||
|
ImBoolVector GlyphsSet; // This is used to resolve collision when multiple sources are merged into a same destination font.
|
||||||
|
};
|
||||||
|
|
||||||
|
static void UnpackBoolVectorToFlatIndexList(const ImBoolVector* in, ImVector<int>* out)
|
||||||
|
{
|
||||||
|
IM_ASSERT(sizeof(in->Storage.Data[0]) == sizeof(int));
|
||||||
|
const int* it_begin = in->Storage.begin();
|
||||||
|
const int* it_end = in->Storage.end();
|
||||||
|
for (const int* it = it_begin; it < it_end; it++)
|
||||||
|
if (int entries_32 = *it)
|
||||||
|
for (int bit_n = 0; bit_n < 32; bit_n++)
|
||||||
|
if (entries_32 & (1 << bit_n))
|
||||||
|
out->push_back((int)((it - it_begin) << 5) + bit_n);
|
||||||
|
}
|
||||||
|
|
||||||
bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas)
|
bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas)
|
||||||
{
|
{
|
||||||
IM_ASSERT(atlas->ConfigData.Size > 0);
|
IM_ASSERT(atlas->ConfigData.Size > 0);
|
||||||
|
|
||||||
ImFontAtlasBuildRegisterDefaultCustomRects(atlas);
|
ImFontAtlasBuildRegisterDefaultCustomRects(atlas);
|
||||||
|
|
||||||
|
// Clear atlas
|
||||||
atlas->TexID = (ImTextureID)NULL;
|
atlas->TexID = (ImTextureID)NULL;
|
||||||
atlas->TexWidth = atlas->TexHeight = 0;
|
atlas->TexWidth = atlas->TexHeight = 0;
|
||||||
atlas->TexUvScale = ImVec2(0.0f, 0.0f);
|
atlas->TexUvScale = ImVec2(0.0f, 0.0f);
|
||||||
atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f);
|
atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f);
|
||||||
atlas->ClearTexData();
|
atlas->ClearTexData();
|
||||||
|
|
||||||
// Count glyphs/ranges
|
// Temporary storage for building
|
||||||
int total_glyphs_count = 0;
|
ImVector<ImFontBuildSrcData> src_tmp_array;
|
||||||
int total_ranges_count = 0;
|
ImVector<ImFontBuildDstData> dst_tmp_array;
|
||||||
for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++)
|
src_tmp_array.resize(atlas->ConfigData.Size);
|
||||||
|
dst_tmp_array.resize(atlas->Fonts.Size);
|
||||||
|
memset(src_tmp_array.Data, 0, (size_t)src_tmp_array.size_in_bytes());
|
||||||
|
memset(dst_tmp_array.Data, 0, (size_t)dst_tmp_array.size_in_bytes());
|
||||||
|
|
||||||
|
// 1. Initialize font loading structure, check font data validity
|
||||||
|
for (int src_i = 0; src_i < atlas->ConfigData.Size; src_i++)
|
||||||
{
|
{
|
||||||
ImFontConfig& cfg = atlas->ConfigData[input_i];
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
if (!cfg.GlyphRanges)
|
ImFontConfig& cfg = atlas->ConfigData[src_i];
|
||||||
cfg.GlyphRanges = atlas->GetGlyphRangesDefault();
|
|
||||||
for (const ImWchar* in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, total_ranges_count++)
|
|
||||||
total_glyphs_count += (in_range[1] - in_range[0]) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need a width for the skyline algorithm. Using a dumb heuristic here to decide of width. User can override TexDesiredWidth and TexGlyphPadding if they wish.
|
|
||||||
// Width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height.
|
|
||||||
atlas->TexWidth = (atlas->TexDesiredWidth > 0) ? atlas->TexDesiredWidth : (total_glyphs_count > 4000) ? 4096 : (total_glyphs_count > 2000) ? 2048 : (total_glyphs_count > 1000) ? 1024 : 512;
|
|
||||||
atlas->TexHeight = 0;
|
|
||||||
|
|
||||||
// Start packing
|
|
||||||
const int max_tex_height = 1024*32;
|
|
||||||
stbtt_pack_context spc = {};
|
|
||||||
if (!stbtt_PackBegin(&spc, NULL, atlas->TexWidth, max_tex_height, 0, atlas->TexGlyphPadding, NULL))
|
|
||||||
return false;
|
|
||||||
stbtt_PackSetOversampling(&spc, 1, 1);
|
|
||||||
|
|
||||||
// Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values).
|
|
||||||
ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info);
|
|
||||||
|
|
||||||
// Initialize font information (so we can error without any cleanup)
|
|
||||||
struct ImFontTempBuildData
|
|
||||||
{
|
|
||||||
stbtt_fontinfo FontInfo;
|
|
||||||
stbrp_rect* Rects;
|
|
||||||
int RectsCount;
|
|
||||||
stbtt_pack_range* Ranges;
|
|
||||||
int RangesCount;
|
|
||||||
};
|
|
||||||
ImFontTempBuildData* tmp_array = (ImFontTempBuildData*)ImGui::MemAlloc((size_t)atlas->ConfigData.Size * sizeof(ImFontTempBuildData));
|
|
||||||
for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++)
|
|
||||||
{
|
|
||||||
ImFontConfig& cfg = atlas->ConfigData[input_i];
|
|
||||||
ImFontTempBuildData& tmp = tmp_array[input_i];
|
|
||||||
IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas));
|
IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas));
|
||||||
|
|
||||||
|
// Find index from cfg.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices)
|
||||||
|
src_tmp.DstIndex = -1;
|
||||||
|
for (int output_i = 0; output_i < atlas->Fonts.Size && src_tmp.DstIndex == -1; output_i++)
|
||||||
|
if (cfg.DstFont == atlas->Fonts[output_i])
|
||||||
|
src_tmp.DstIndex = output_i;
|
||||||
|
IM_ASSERT(src_tmp.DstIndex != -1); // cfg.DstFont not pointing within atlas->Fonts[] array?
|
||||||
|
if (src_tmp.DstIndex == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Initialize helper structure for font loading and verify that the TTF/OTF data is correct
|
||||||
const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo);
|
const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo);
|
||||||
IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found.");
|
IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found.");
|
||||||
if (!stbtt_InitFont(&tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset))
|
if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset))
|
||||||
{
|
|
||||||
atlas->TexWidth = atlas->TexHeight = 0; // Reset output on failure
|
|
||||||
ImGui::MemFree(tmp_array);
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Measure highest codepoints
|
||||||
|
ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex];
|
||||||
|
src_tmp.SrcRanges = cfg.GlyphRanges ? cfg.GlyphRanges : atlas->GetGlyphRangesDefault();
|
||||||
|
for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2)
|
||||||
|
src_tmp.GlyphsHighest = ImMax(src_tmp.GlyphsHighest, (int)src_range[1]);
|
||||||
|
dst_tmp.SrcCount++;
|
||||||
|
dst_tmp.GlyphsHighest = ImMax(dst_tmp.GlyphsHighest, src_tmp.GlyphsHighest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. For every requested codepoint, check for their presence in the font data, and handle redundancy or overlaps between source fonts to avoid unused glyphs.
|
||||||
|
int total_glyphs_count = 0;
|
||||||
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
|
{
|
||||||
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
|
ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex];
|
||||||
|
ImFontConfig& cfg = atlas->ConfigData[src_i];
|
||||||
|
src_tmp.GlyphsSet.Resize(src_tmp.GlyphsHighest);
|
||||||
|
if (dst_tmp.SrcCount > 1 && dst_tmp.GlyphsSet.Storage.empty())
|
||||||
|
dst_tmp.GlyphsSet.Resize(dst_tmp.GlyphsHighest);
|
||||||
|
|
||||||
|
for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2)
|
||||||
|
for (int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++)
|
||||||
|
{
|
||||||
|
if (cfg.MergeMode && dst_tmp.GlyphsSet.GetBit(codepoint)) // Don't overwrite existing glyphs. We could make this an option (e.g. MergeOverwrite)
|
||||||
|
continue;
|
||||||
|
if (!stbtt_FindGlyphIndex(&src_tmp.FontInfo, codepoint)) // It is actually in the font?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Add to avail set/counters
|
||||||
|
src_tmp.GlyphsCount++;
|
||||||
|
dst_tmp.GlyphsCount++;
|
||||||
|
src_tmp.GlyphsSet.SetBit(codepoint, true);
|
||||||
|
if (dst_tmp.SrcCount > 1)
|
||||||
|
dst_tmp.GlyphsSet.SetBit(codepoint, true);
|
||||||
|
total_glyphs_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Unpack our bit map into a flat list (we now have all the Unicode points that we know are requested _and_ available _and_ not overlapping another)
|
||||||
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
|
{
|
||||||
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
|
src_tmp.GlyphsList.reserve(src_tmp.GlyphsCount);
|
||||||
|
UnpackBoolVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList);
|
||||||
|
src_tmp.GlyphsSet.Clear();
|
||||||
|
IM_ASSERT(src_tmp.GlyphsList.Size == src_tmp.GlyphsCount);
|
||||||
|
}
|
||||||
|
for (int dst_i = 0; dst_i < dst_tmp_array.Size; dst_i++)
|
||||||
|
dst_tmp_array[dst_i].GlyphsSet.Clear();
|
||||||
|
dst_tmp_array.clear();
|
||||||
|
|
||||||
// Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0)
|
// Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0)
|
||||||
int buf_packedchars_n = 0, buf_rects_n = 0, buf_ranges_n = 0;
|
// (We technically don't need to zero-clear buf_rects, but let's do it for the sake of sanity)
|
||||||
stbtt_packedchar* buf_packedchars = (stbtt_packedchar*)ImGui::MemAlloc(total_glyphs_count * sizeof(stbtt_packedchar));
|
ImVector<stbrp_rect> buf_rects;
|
||||||
stbrp_rect* buf_rects = (stbrp_rect*)ImGui::MemAlloc(total_glyphs_count * sizeof(stbrp_rect));
|
ImVector<stbtt_packedchar> buf_packedchars;
|
||||||
stbtt_pack_range* buf_ranges = (stbtt_pack_range*)ImGui::MemAlloc(total_ranges_count * sizeof(stbtt_pack_range));
|
buf_rects.resize(total_glyphs_count);
|
||||||
memset(buf_packedchars, 0, total_glyphs_count * sizeof(stbtt_packedchar));
|
buf_packedchars.resize(total_glyphs_count);
|
||||||
memset(buf_rects, 0, total_glyphs_count * sizeof(stbrp_rect)); // Unnecessary but let's clear this for the sake of sanity.
|
memset(buf_rects.Data, 0, (size_t)buf_rects.size_in_bytes());
|
||||||
memset(buf_ranges, 0, total_ranges_count * sizeof(stbtt_pack_range));
|
memset(buf_packedchars.Data, 0, (size_t)buf_packedchars.size_in_bytes());
|
||||||
|
|
||||||
// First font pass: pack all glyphs (no rendering at this point, we are working with rectangles in an infinitely tall texture at this point)
|
// 4. Gather glyphs sizes so we can pack them in our virtual canvas.
|
||||||
for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++)
|
int total_surface = 0;
|
||||||
|
int buf_rects_out_n = 0;
|
||||||
|
int buf_packedchars_out_n = 0;
|
||||||
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
{
|
{
|
||||||
ImFontConfig& cfg = atlas->ConfigData[input_i];
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
ImFontTempBuildData& tmp = tmp_array[input_i];
|
if (src_tmp.GlyphsCount == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Setup ranges
|
src_tmp.Rects = &buf_rects[buf_rects_out_n];
|
||||||
int font_glyphs_count = 0;
|
src_tmp.PackedChars = &buf_packedchars[buf_packedchars_out_n];
|
||||||
int font_ranges_count = 0;
|
buf_rects_out_n += src_tmp.GlyphsCount;
|
||||||
for (const ImWchar* in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, font_ranges_count++)
|
buf_packedchars_out_n += src_tmp.GlyphsCount;
|
||||||
font_glyphs_count += (in_range[1] - in_range[0]) + 1;
|
|
||||||
tmp.Ranges = buf_ranges + buf_ranges_n;
|
// Convert our ranges in the format stb_truetype wants
|
||||||
tmp.RangesCount = font_ranges_count;
|
ImFontConfig& cfg = atlas->ConfigData[src_i];
|
||||||
buf_ranges_n += font_ranges_count;
|
src_tmp.PackRange.font_size = cfg.SizePixels;
|
||||||
for (int i = 0; i < font_ranges_count; i++)
|
src_tmp.PackRange.first_unicode_codepoint_in_range = 0;
|
||||||
|
src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data;
|
||||||
|
src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size;
|
||||||
|
src_tmp.PackRange.chardata_for_range = src_tmp.PackedChars;
|
||||||
|
src_tmp.PackRange.h_oversample = (unsigned char)cfg.OversampleH;
|
||||||
|
src_tmp.PackRange.v_oversample = (unsigned char)cfg.OversampleV;
|
||||||
|
|
||||||
|
// Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects)
|
||||||
|
const float scale = (cfg.SizePixels > 0) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels);
|
||||||
|
const int padding = atlas->TexGlyphPadding;
|
||||||
|
for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++)
|
||||||
{
|
{
|
||||||
const ImWchar* in_range = &cfg.GlyphRanges[i * 2];
|
int x0, y0, x1, y1;
|
||||||
stbtt_pack_range& range = tmp.Ranges[i];
|
const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]);
|
||||||
range.font_size = cfg.SizePixels;
|
IM_ASSERT(glyph_index_in_font != 0);
|
||||||
range.first_unicode_codepoint_in_range = in_range[0];
|
stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * cfg.OversampleH, scale * cfg.OversampleV, 0, 0, &x0, &y0, &x1, &y1);
|
||||||
range.num_chars = (in_range[1] - in_range[0]) + 1;
|
src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + padding + cfg.OversampleH - 1);
|
||||||
range.chardata_for_range = buf_packedchars + buf_packedchars_n;
|
src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + padding + cfg.OversampleV - 1);
|
||||||
buf_packedchars_n += range.num_chars;
|
total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h;
|
||||||
}
|
|
||||||
|
|
||||||
// Gather the sizes of all rectangle we need
|
|
||||||
tmp.Rects = buf_rects + buf_rects_n;
|
|
||||||
tmp.RectsCount = font_glyphs_count;
|
|
||||||
buf_rects_n += font_glyphs_count;
|
|
||||||
stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV);
|
|
||||||
int n = stbtt_PackFontRangesGatherRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects);
|
|
||||||
IM_ASSERT(n == font_glyphs_count);
|
|
||||||
|
|
||||||
// Detect missing glyphs and replace them with a zero-sized box instead of relying on the default glyphs
|
|
||||||
// This allows us merging overlapping icon fonts more easily.
|
|
||||||
int rect_i = 0;
|
|
||||||
for (int range_i = 0; range_i < tmp.RangesCount; range_i++)
|
|
||||||
for (int char_i = 0; char_i < tmp.Ranges[range_i].num_chars; char_i++, rect_i++)
|
|
||||||
if (stbtt_FindGlyphIndex(&tmp.FontInfo, tmp.Ranges[range_i].first_unicode_codepoint_in_range + char_i) == 0)
|
|
||||||
tmp.Rects[rect_i].w = tmp.Rects[rect_i].h = 0;
|
|
||||||
|
|
||||||
// Pack
|
|
||||||
stbrp_pack_rects((stbrp_context*)spc.pack_info, tmp.Rects, n);
|
|
||||||
|
|
||||||
// Extend texture height
|
|
||||||
// Also mark missing glyphs as non-packed so we don't attempt to render into them
|
|
||||||
for (int i = 0; i < n; i++)
|
|
||||||
{
|
|
||||||
if (tmp.Rects[i].w == 0 && tmp.Rects[i].h == 0)
|
|
||||||
tmp.Rects[i].was_packed = 0;
|
|
||||||
if (tmp.Rects[i].was_packed)
|
|
||||||
atlas->TexHeight = ImMax(atlas->TexHeight, tmp.Rects[i].y + tmp.Rects[i].h);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IM_ASSERT(buf_rects_n == total_glyphs_count);
|
|
||||||
IM_ASSERT(buf_packedchars_n == total_glyphs_count);
|
|
||||||
IM_ASSERT(buf_ranges_n == total_ranges_count);
|
|
||||||
|
|
||||||
// Create texture
|
// We need a width for the skyline algorithm, any width!
|
||||||
|
// The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height.
|
||||||
|
// User can override TexDesiredWidth and TexGlyphPadding if they wish, otherwise we use a simple heuristic to select the width based on expected surface.
|
||||||
|
const int surface_sqrt = (int)ImSqrt((float)total_surface) + 1;
|
||||||
|
atlas->TexHeight = 0;
|
||||||
|
if (atlas->TexDesiredWidth > 0)
|
||||||
|
atlas->TexWidth = atlas->TexDesiredWidth;
|
||||||
|
else
|
||||||
|
atlas->TexWidth = (surface_sqrt >= 4096*0.7f) ? 4096 : (surface_sqrt >= 2048*0.7f) ? 2048 : (surface_sqrt >= 1024*0.7f) ? 1024 : 512;
|
||||||
|
|
||||||
|
// 5. Start packing
|
||||||
|
// Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values).
|
||||||
|
const int TEX_HEIGHT_MAX = 1024 * 32;
|
||||||
|
stbtt_pack_context spc = {};
|
||||||
|
stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, atlas->TexGlyphPadding, NULL);
|
||||||
|
ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info);
|
||||||
|
|
||||||
|
// 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point.
|
||||||
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
|
{
|
||||||
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
|
if (src_tmp.GlyphsCount == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
stbrp_pack_rects((stbrp_context*)spc.pack_info, src_tmp.Rects, src_tmp.GlyphsCount);
|
||||||
|
|
||||||
|
// Extend texture height and mark missing glyphs as non-packed so we won't render them.
|
||||||
|
// FIXME: We are not handling packing failure here (would happen if we got off TEX_HEIGHT_MAX or if a single if larger than TexWidth?)
|
||||||
|
for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++)
|
||||||
|
if (src_tmp.Rects[glyph_i].was_packed)
|
||||||
|
atlas->TexHeight = ImMax(atlas->TexHeight, src_tmp.Rects[glyph_i].y + src_tmp.Rects[glyph_i].h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Allocate texture
|
||||||
atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight);
|
atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight);
|
||||||
atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight);
|
atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight);
|
||||||
atlas->TexPixelsAlpha8 = (unsigned char*)ImGui::MemAlloc(atlas->TexWidth * atlas->TexHeight);
|
atlas->TexPixelsAlpha8 = (unsigned char*)ImGui::MemAlloc(atlas->TexWidth * atlas->TexHeight);
|
||||||
@ -1873,41 +1954,46 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas)
|
|||||||
spc.pixels = atlas->TexPixelsAlpha8;
|
spc.pixels = atlas->TexPixelsAlpha8;
|
||||||
spc.height = atlas->TexHeight;
|
spc.height = atlas->TexHeight;
|
||||||
|
|
||||||
// Second pass: render font characters
|
// 8. Render/rasterize font characters into the texture
|
||||||
for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++)
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
{
|
{
|
||||||
ImFontConfig& cfg = atlas->ConfigData[input_i];
|
ImFontConfig& cfg = atlas->ConfigData[src_i];
|
||||||
ImFontTempBuildData& tmp = tmp_array[input_i];
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV);
|
if (src_tmp.GlyphsCount == 0)
|
||||||
stbtt_PackFontRangesRenderIntoRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects);
|
continue;
|
||||||
|
|
||||||
|
stbtt_PackFontRangesRenderIntoRects(&spc, &src_tmp.FontInfo, &src_tmp.PackRange, 1, src_tmp.Rects);
|
||||||
|
|
||||||
|
// Apply multiply operator
|
||||||
if (cfg.RasterizerMultiply != 1.0f)
|
if (cfg.RasterizerMultiply != 1.0f)
|
||||||
{
|
{
|
||||||
unsigned char multiply_table[256];
|
unsigned char multiply_table[256];
|
||||||
ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply);
|
ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply);
|
||||||
for (const stbrp_rect* r = tmp.Rects; r != tmp.Rects + tmp.RectsCount; r++)
|
stbrp_rect* r = &src_tmp.Rects[0];
|
||||||
|
for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++, r++)
|
||||||
if (r->was_packed)
|
if (r->was_packed)
|
||||||
ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, spc.pixels, r->x, r->y, r->w, r->h, spc.stride_in_bytes);
|
ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, atlas->TexPixelsAlpha8, r->x, r->y, r->w, r->h, atlas->TexWidth * 1);
|
||||||
}
|
}
|
||||||
tmp.Rects = NULL;
|
src_tmp.Rects = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End packing
|
// End packing
|
||||||
stbtt_PackEnd(&spc);
|
stbtt_PackEnd(&spc);
|
||||||
ImGui::MemFree(buf_rects);
|
buf_rects.clear();
|
||||||
buf_rects = NULL;
|
|
||||||
|
|
||||||
// Third pass: setup ImFont and glyphs for runtime
|
// 9. Setup ImFont and glyphs for runtime
|
||||||
for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++)
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
{
|
{
|
||||||
ImFontConfig& cfg = atlas->ConfigData[input_i];
|
ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
|
||||||
ImFontTempBuildData& tmp = tmp_array[input_i];
|
if (src_tmp.GlyphsCount == 0)
|
||||||
ImFont* dst_font = cfg.DstFont; // We can have multiple input fonts writing into a same destination font (when using MergeMode=true)
|
continue;
|
||||||
if (cfg.MergeMode)
|
|
||||||
dst_font->BuildLookupTable();
|
|
||||||
|
|
||||||
const float font_scale = stbtt_ScaleForPixelHeight(&tmp.FontInfo, cfg.SizePixels);
|
ImFontConfig& cfg = atlas->ConfigData[src_i];
|
||||||
|
ImFont* dst_font = cfg.DstFont; // We can have multiple input fonts writing into a same destination font (when using MergeMode=true)
|
||||||
|
|
||||||
|
const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels);
|
||||||
int unscaled_ascent, unscaled_descent, unscaled_line_gap;
|
int unscaled_ascent, unscaled_descent, unscaled_line_gap;
|
||||||
stbtt_GetFontVMetrics(&tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap);
|
stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap);
|
||||||
|
|
||||||
const float ascent = ImFloor(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1));
|
const float ascent = ImFloor(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1));
|
||||||
const float descent = ImFloor(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1));
|
const float descent = ImFloor(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1));
|
||||||
@ -1915,40 +2001,30 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas)
|
|||||||
const float font_off_x = cfg.GlyphOffset.x;
|
const float font_off_x = cfg.GlyphOffset.x;
|
||||||
const float font_off_y = cfg.GlyphOffset.y + (float)(int)(dst_font->Ascent + 0.5f);
|
const float font_off_y = cfg.GlyphOffset.y + (float)(int)(dst_font->Ascent + 0.5f);
|
||||||
|
|
||||||
for (int i = 0; i < tmp.RangesCount; i++)
|
for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++)
|
||||||
{
|
{
|
||||||
stbtt_pack_range& range = tmp.Ranges[i];
|
const int codepoint = src_tmp.GlyphsList[glyph_i];
|
||||||
for (int char_idx = 0; char_idx < range.num_chars; char_idx += 1)
|
const stbtt_packedchar& pc = src_tmp.PackedChars[glyph_i];
|
||||||
{
|
|
||||||
const stbtt_packedchar& pc = range.chardata_for_range[char_idx];
|
|
||||||
if (!pc.x0 && !pc.x1 && !pc.y0 && !pc.y1)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const int codepoint = range.first_unicode_codepoint_in_range + char_idx;
|
const float char_advance_x_org = pc.xadvance;
|
||||||
if (cfg.MergeMode && dst_font->FindGlyphNoFallback((ImWchar)codepoint))
|
const float char_advance_x_mod = ImClamp(char_advance_x_org, cfg.GlyphMinAdvanceX, cfg.GlyphMaxAdvanceX);
|
||||||
continue;
|
|
||||||
|
|
||||||
float char_advance_x_org = pc.xadvance;
|
|
||||||
float char_advance_x_mod = ImClamp(char_advance_x_org, cfg.GlyphMinAdvanceX, cfg.GlyphMaxAdvanceX);
|
|
||||||
float char_off_x = font_off_x;
|
float char_off_x = font_off_x;
|
||||||
if (char_advance_x_org != char_advance_x_mod)
|
if (char_advance_x_org != char_advance_x_mod)
|
||||||
char_off_x += cfg.PixelSnapH ? (float)(int)((char_advance_x_mod - char_advance_x_org) * 0.5f) : (char_advance_x_mod - char_advance_x_org) * 0.5f;
|
char_off_x += cfg.PixelSnapH ? (float)(int)((char_advance_x_mod - char_advance_x_org) * 0.5f) : (char_advance_x_mod - char_advance_x_org) * 0.5f;
|
||||||
|
|
||||||
|
// Register glyph
|
||||||
stbtt_aligned_quad q;
|
stbtt_aligned_quad q;
|
||||||
float dummy_x = 0.0f, dummy_y = 0.0f;
|
float dummy_x = 0.0f, dummy_y = 0.0f;
|
||||||
stbtt_GetPackedQuad(range.chardata_for_range, atlas->TexWidth, atlas->TexHeight, char_idx, &dummy_x, &dummy_y, &q, 0);
|
stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &dummy_x, &dummy_y, &q, 0);
|
||||||
dst_font->AddGlyph((ImWchar)codepoint, q.x0 + char_off_x, q.y0 + font_off_y, q.x1 + char_off_x, q.y1 + font_off_y, q.s0, q.t0, q.s1, q.t1, char_advance_x_mod);
|
dst_font->AddGlyph((ImWchar)codepoint, q.x0 + char_off_x, q.y0 + font_off_y, q.x1 + char_off_x, q.y1 + font_off_y, q.s0, q.t0, q.s1, q.t1, char_advance_x_mod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup temporaries
|
// Cleanup temporary (ImVector doesn't honor destructor)
|
||||||
ImGui::MemFree(buf_packedchars);
|
for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
|
||||||
ImGui::MemFree(buf_ranges);
|
src_tmp_array[src_i].~ImFontBuildSrcData();
|
||||||
ImGui::MemFree(tmp_array);
|
|
||||||
|
|
||||||
ImFontAtlasBuildFinish(atlas);
|
ImFontAtlasBuildFinish(atlas);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1976,16 +2052,16 @@ void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* f
|
|||||||
font->ConfigDataCount++;
|
font->ConfigDataCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* pack_context_opaque)
|
void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque)
|
||||||
{
|
{
|
||||||
stbrp_context* pack_context = (stbrp_context*)pack_context_opaque;
|
stbrp_context* pack_context = (stbrp_context*)stbrp_context_opaque;
|
||||||
|
|
||||||
ImVector<ImFontAtlas::CustomRect>& user_rects = atlas->CustomRects;
|
ImVector<ImFontAtlas::CustomRect>& user_rects = atlas->CustomRects;
|
||||||
IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong.
|
IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong.
|
||||||
|
|
||||||
ImVector<stbrp_rect> pack_rects;
|
ImVector<stbrp_rect> pack_rects;
|
||||||
pack_rects.resize(user_rects.Size);
|
pack_rects.resize(user_rects.Size);
|
||||||
memset(pack_rects.Data, 0, sizeof(stbrp_rect) * user_rects.Size);
|
memset(pack_rects.Data, 0, (size_t)pack_rects.size_in_bytes());
|
||||||
for (int i = 0; i < user_rects.Size; i++)
|
for (int i = 0; i < user_rects.Size; i++)
|
||||||
{
|
{
|
||||||
pack_rects[i].w = user_rects[i].Width;
|
pack_rects[i].w = user_rects[i].Width;
|
||||||
|
@ -1452,7 +1452,7 @@ namespace ImGui
|
|||||||
IMGUI_API bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas);
|
IMGUI_API bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas);
|
||||||
IMGUI_API void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas);
|
IMGUI_API void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas);
|
||||||
IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent);
|
IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent);
|
||||||
IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* spc);
|
IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque);
|
||||||
IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas);
|
IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas);
|
||||||
IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor);
|
IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor);
|
||||||
IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride);
|
IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride);
|
||||||
|
Loading…
Reference in New Issue
Block a user