dlib/fonts.d
2026-04-17 18:21:52 +10:00

821 lines
18 KiB
D

module dlib.fonts;
import dlibincludes;
import dlib.aliases;
import dlib.util;
import dlib.alloc;
import dlib.math;
import std.math.traits : isNaN;
import std.math : Floor = floor;
import std.algorithm.comparison : clamp;
import std.algorithm.sorting;
const u64 FONT_MAX_BANDS = 8;
const u64 FONT_TEX_WIDTH = 4096;
struct SlugTexel(T)
{
T[] data;
u32 height;
u32 width;
}
struct SlugCurveRef
{
u32 index;
f32 sort_key;
}
struct SlugBandEntry
{
u32[] indices;
u32 count;
}
struct SlugGlyphBandData
{
SlugBandEntry[FONT_MAX_BANDS][2] bands;
U8Vec2 band_counts;
}
struct SlugCurve
{
Vec2 p1, p2, p3;
}
struct SlugRect
{
Vec2 min, max;
}
struct SlugGlyph
{
SlugRect rect;
IVec2 bearing;
IVec2 size;
Vec2 band_scale;
Vec2 band_offset;
i32 glyph_index;
i32 advance;
i32 curve_start;
i32 curve_count;
U16Vec2 coords;
U8Vec2 max_bands;
}
struct SlugFontInfo
{
SlugCurve[] curves;
SlugTexel!(f32) curve_texel;
SlugTexel!(u32) band_texel;
SlugGlyphBandData[] glyph_bands;
SlugFont base;
alias base this;
}
struct SlugFont
{
SlugGlyph[128] glyphs;
f32 scale;
f32 line_height;
i32 ascent;
i32 descent;
i32 line_gap;
}
struct SlugVertex
{
union
{
Vec4 pos;
struct
{
Vec2 vertex_coords;
Vec2 normal;
};
};
union
{
Vec4 tex;
struct
{
Vec2 em_space_pos;
U16Vec2 glyph_data_coords;
struct max_bands
{
u8 x, pad, y, e;
};
}
};
union
{
Vec4 jac;
struct
{
f32 p00, p01, p10, p11;
};
};
union
{
Vec4 band;
struct
{
Vec4 scale;
Vec4 offset;
};
};
Vec4 color;
}
struct FontAtlas
{
Glyph[128] glyphs;
u8[] data;
u32 size;
UVec2 dimensions;
f32 ascent;
f32 descent;
f32 line_gap;
f32 line_height;
f32 max_advance;
}
struct Glyph
{
dchar ch;
f32 advance;
f32 plane_left;
f32 plane_bottom;
f32 plane_right;
f32 plane_top;
f32 atlas_left;
f32 atlas_bottom;
f32 atlas_right;
f32 atlas_top;
}
__gshared FT_Library FT_LIB;
alias FontFace = FT_Face;
shared static this()
{
FT_Init_FreeType(&FT_LIB);
}
void
CloseFreeType()
{
if(FT_LIB)
{
FT_Done_FreeType(FT_LIB);
}
}
FontFace
OpenFont(u8[] data)
{
FontFace font;
FT_New_Memory_Face(FT_LIB, data.ptr, cast(FT_Long)data.length, 0, &font);
return font;
}
void
CloseFont(FontFace font)
{
if(font != null)
{
FT_Done_Face(font);
}
}
i32
Float26(f32 f)
{
return cast(i32)round(cast(f32)(1 << 6) * f);
}
u8
DeMultiply(u8 color, u8 alpha)
{
u32 v = cast(u32)(255.0 * cast(f32)(color) / cast(f32)(alpha + f32.epsilon) + 0.5);
return v > 255 ? 255 : cast(u8)v;
}
u32
TextSize(f32 s)
{
return cast(u32)((96.0f/72.0f) * s);
}
FontAtlas
CreateAtlas(Arena* arena, FontFace font, u32 size, UVec2 atlas_dim)
{
FontAtlas atlas = {
data: Alloc!(u8)(arena, atlas_dim.x * atlas_dim.y * 4),
size: size,
dimensions: atlas_dim,
};
// TODO: proper packing algorithm
if(font != null)
{
FT_Set_Pixel_Sizes(font, 0, TextSize(size));
i64 bbox_ymax = font.bbox.yMax >> 6;
i64 bbox_ymin = font.bbox.yMin >> 6;
u64 baseline = font.size.metrics.ascender >> 6;
u64 line_height = (font.size.metrics.height >> 6)+1;
atlas.line_height = line_height;
atlas.max_advance = font.size.metrics.max_advance >> 6;
u32 max_w, max_h, count;
i32 font_size = Float26(size);
const u32 PADDING = 2;
foreach(FT_ULong char_code; 0 .. 0x7F)
{
FT_Error res = FT_Load_Char(font, char_code, cast(FT_Int32)FT_LOAD_RENDER);
if(res != 0)
{
continue;
}
u32 bmp_w = font.glyph.bitmap.width;
u32 bmp_h = font.glyph.bitmap.rows;
if(max_w + bmp_w > atlas_dim.x)
{
max_h += line_height+PADDING;
max_w = 0;
}
if(max_h > atlas_dim.y)
{
Logf("Unable to pack atlas within dimensions provided");
assert(false);
}
max_w += bmp_w;
count += 1;
}
max_w = max_h = count = 0;
foreach(FT_ULong char_code; 0 .. 0x7F)
{
FT_Error res = FT_Load_Char(font, char_code, cast(FT_Int32)FT_LOAD_RENDER);
if(res != 0)
{
continue;
}
FT_GlyphSlot glyph = font.glyph;
FT_Bitmap* bmp = &font.glyph.bitmap;
if(max_w + bmp.rows > atlas_dim.x)
{
max_h += line_height+PADDING;
max_w = PADDING;
}
i64 base = Max(baseline, font.glyph.bitmap_top);
switch(bmp.pixel_mode)
{
case FT_PIXEL_MODE_BGRA:
{
u64 x, y;
foreach(r; 0 .. bmp.rows)
{
y = (base - font.glyph.bitmap_top + r) + max_h;
foreach(c; 0 .. bmp.width)
{
x = max_w + c;
u64 offset = (y*atlas_dim.y + x) * 4;
u8 p_b = bmp.buffer[offset+0];
u8 p_g = bmp.buffer[offset+1];
u8 p_r = bmp.buffer[offset+2];
u8 p_a = bmp.buffer[offset+3];
atlas.data[offset .. offset+4] = [DeMultiply(p_r, p_a), DeMultiply(p_b, p_a), DeMultiply(p_g, p_a), p_a];
}
}
} break;
case FT_PIXEL_MODE_MONO:
{
u32 pitch = bmp.pitch;
u8* buf = bmp.buffer;
u64 x, y;
foreach(r; 0 .. bmp.rows)
{
u8 bits = 0;
u8* buf_ptr = buf;
y = (base - font.glyph.bitmap_top + r) + max_h;
foreach(c; 0 .. bmp.width)
{
if((c & 7) == 0)
{
bits = *buf_ptr;
buf_ptr += 1;
}
x = max_w + c;
u64 offset = (y*atlas_dim.y + x) * 4;
atlas.data[offset .. offset+4] = [255, 255, 255, bits & 0x80 ? 255 : 0];
bits <<= 1;
}
buf += pitch;
}
} break;
case FT_PIXEL_MODE_GRAY:
{
u64 x, y;
foreach(r; 0 .. bmp.rows)
{
y = (base - font.glyph.bitmap_top + r) + max_h;
foreach(c; 0 .. bmp.width)
{
x = max_w + c;
u64 offset = (y*atlas_dim.y + x) * 4;
atlas.data[offset .. offset+4] = [255, 255, 255, bmp.buffer[r*bmp.pitch + c]];
}
}
} break;
default:
assert(false, "Unknown pixel_mode value");
break;
}
Glyph* g = atlas.glyphs.ptr + char_code;
f32 height = glyph.metrics.height >> 6;
f32 width = glyph.metrics.width >> 6;
f32 top = font.glyph.bitmap_top;
f32 left = font.glyph.bitmap_left;
g.ch = cast(dchar)char_code;
g.advance = cast(f32)(glyph.advance.x >> 6);
g.plane_left = left;
g.plane_right = g.plane_left + bmp.width;
g.plane_top = 0.0;
g.plane_bottom = line_height;
g.atlas_top = max_h;
g.atlas_left = max_w;
g.atlas_bottom = max_h + line_height;
g.atlas_right = max_w + bmp.width;
max_w += bmp.width + PADDING;
count += 1;
}
}
return atlas;
}
void
BuildFontGlyph(Arena* arena, u32 index, u32 glyph_index, SlugFontInfo* font_info, u32* curve_index, stbtt_fontinfo* stb_font_info)
{
SlugGlyph* glyph = &font_info.glyphs[index];
glyph.glyph_index = glyph_index;
glyph.curve_start = *curve_index;
stbtt_vertex* vertices;
u32 vertex_count = stbtt_GetGlyphShape(stb_font_info, glyph_index, &vertices);
if(vertex_count > 0)
{
f32 cx = 0.0, cy = 0.0;
foreach(i; 0 .. vertex_count)
{
stbtt_vertex* vtx = &vertices[i];
f32 x = cast(f32)(vtx.x);
f32 y = cast(f32)(vtx.y);
switch(vtx.type)
{
case STBTT_vmove:
{
cx = x;
cy = y;
} break;
case STBTT_vline:
{
f32 dx = x-cx;
f32 dy = y-cy;
if(fabs(dx) < 0.1 && fabs(dy) < 0.1)
{
cx = x;
cy = y;
break;
}
f32 length = sqrt(dx*dx + dy*dy);
f32 nx = -dy/length*0.05;
f32 ny = -dx/length*0.05;
SlugCurve* curve = &font_info.curves[*curve_index];
*curve = SlugCurve(
Vec2(cx, cy),
Vec2((cx+x) * 0.5 + nx, (cy+y) * 0.5 + ny),
Vec2(x, y),
);
cx = x;
cy = y;
*curve_index += 1;
} break;
case STBTT_vcurve:
{
SlugCurve* curve = &font_info.curves[*curve_index];
*curve = SlugCurve(
Vec2(cx, cy),
Vec2(vtx.cx, vtx.cy),
Vec2(x, y),
);
*curve_index += 1;
} break;
case STBTT_vcubic:
{
f32 cx1 = cast(f32)(vtx.cx );
f32 cy1 = cast(f32)(vtx.cy );
f32 cx2 = cast(f32)(vtx.cx1);
f32 cy2 = cast(f32)(vtx.cy1);
Vec2 m01 = Vec2(cx+cx1, cy+cy1 ) * 0.5;
Vec2 m12 = Vec2(cx1+cx2, cy1+cy2) * 0.5;
Vec2 m23 = Vec2(cx2+x, cy2+y ) * 0.5;
Vec2 m012 = (m01+m12) * 0.5;
Vec2 m123 = (m12+m23) * 0.5;
Vec2 mid = (m012+m123) * 0.5;
font_info.curves[(*curve_index)+0] = SlugCurve( Vec2(cx, cy), m01, mid );
font_info.curves[(*curve_index)+1] = SlugCurve( mid, m123, Vec2(x, y) );
cx = x;
cy = y;
*curve_index += 2;
} break;
default: break;
}
}
stbtt_FreeShape(stb_font_info, vertices);
}
glyph.curve_count = *curve_index - glyph.curve_start;
i32 advance_width, left_side_bearing;
stbtt_GetGlyphHMetrics(stb_font_info, glyph_index, &advance_width, &left_side_bearing);
glyph.advance = advance_width;
IVec2 p0, p1;
if(!stbtt_GetGlyphBox(stb_font_info, glyph_index, &p0.x, &p0.y, &p1.x, &p1.y))
{
p0 = p1 = 0;
}
glyph.bearing = p0;
glyph.size = p1-p0;
glyph.rect.min = Vec2(p0.x, p0.y);
glyph.rect.max = Vec2(p1.x, p1.y);
glyph.coords = 0;
glyph.max_bands = 0;
glyph.band_scale = 0;
glyph.band_offset = 0;
SlugBuildBands(arena, glyph_index, font_info, FONT_MAX_BANDS);
}
bool
LoadFontCurves(Arena* arena, SlugFontInfo* font_info, u8[] font_file_data)
{
bool result = true;
u32 curve_count;
stbtt_fontinfo stb_font_info;
if(!stbtt_InitFont(&stb_font_info, font_file_data.ptr, stbtt_GetFontOffsetForIndex(font_file_data.ptr, 0)))
{
Errf("Failed to load font data");
result = false;
}
if(result)
{
foreach(i; 0 .. font_info.glyphs.length)
{
curve_count += SlugCountCurvesForGlyph(stbtt_FindGlyphIndex(&stb_font_info, cast(i32)i), &stb_font_info);
}
font_info.curves = Alloc!(SlugCurve)(arena, curve_count+1);
font_info.glyph_bands = Alloc!(SlugGlyphBandData)(arena, font_info.glyphs.length);
f32 font_scale = stbtt_ScaleForPixelHeight(&stb_font_info, 200.0);
stbtt_GetFontVMetrics(&stb_font_info, &font_info.ascent, &font_info.descent, &font_info.line_gap);
font_info.scale = font_scale;
font_info.line_height = cast(f32)(font_info.ascent-font_info.descent);
u32 curve_index = 1;
u32 curve_texel_count;
foreach(i; 0 .. font_info.glyphs.length)
{
BuildFontGlyph(arena, cast(u32)i, stbtt_FindGlyphIndex(&stb_font_info, cast(int)i), font_info, &curve_index, &stb_font_info);
curve_texel_count += font_info.glyphs[i].curve_count*2;
}
u32 curve_texel_height = clamp(cast(u32)((curve_texel_count+FONT_TEX_WIDTH-1) / FONT_TEX_WIDTH), 1, u32.max);
u32[] curve_start_indices = Alloc!(u32)(arena, font_info.glyphs.length);
SlugTexel!(f32) curve_texel = {
data: Alloc!(f32)(arena, FONT_TEX_WIDTH*curve_texel_height*4),
height: curve_texel_height,
width: FONT_TEX_WIDTH,
};
curve_index = 1;
foreach(i; 0 .. font_info.glyphs.length)
{
SlugGlyph* glyph = &font_info.glyphs[i];
curve_start_indices[i] = curve_index;
SlugCurve[] glyph_curves = font_info.curves[glyph.curve_start .. glyph.curve_start+glyph.curve_count];
foreach(j; 0 .. glyph.curve_count)
{
SlugCurve* curve = &glyph_curves[j];
UVec2 t0 = UVec2(curve_index%FONT_TEX_WIDTH, curve_index/FONT_TEX_WIDTH);
u32 offset = cast(u32)((t0.y*FONT_TEX_WIDTH + t0.x) * 4);
curve_texel.data[offset .. offset+4] = [curve.p1.x, curve.p1.y, curve.p2.x, curve.p2.y];
curve_index += 1;
UVec2 t1 = UVec2(curve_index%FONT_TEX_WIDTH, curve_index/FONT_TEX_WIDTH);
offset = cast(u32)((t1.y*FONT_TEX_WIDTH + t1.x) * 4);
curve_texel.data[offset .. offset+4] = [curve.p3.x, curve.p3.y, 0.0, 0.0];
curve_index += 1;
}
}
u32 band_total_count;
foreach(i; 0 .. font_info.glyphs.length)
{
SlugGlyph* glyph = &font_info.glyphs[i];
SlugGlyphBandData* band_data = &font_info.glyph_bands[i];
u32 band_total = band_data.band_counts[0] + band_data.band_counts[1];
u32 padding = cast(u32)(FONT_TEX_WIDTH - (band_total_count%FONT_TEX_WIDTH));
if(padding < band_total && padding < FONT_TEX_WIDTH)
{
band_total_count += padding;
}
band_total_count += band_total;
foreach(axis; 0 .. 2)
{
foreach(j; 0 .. band_data.band_counts[axis])
{
band_total_count += band_data.bands[axis][j].count;
}
}
}
u32 band_texel_height = clamp(cast(u32)((band_total_count+FONT_TEX_WIDTH-1) / FONT_TEX_WIDTH), 1U, u32.max);
SlugTexel!(u32) band_texel = {
data: Alloc!(u32)(arena, FONT_TEX_WIDTH*band_texel_height*4),
width: FONT_TEX_WIDTH,
height: band_texel_height,
};
u32 band_index;
foreach(i; 0 .. font_info.glyphs.length)
{
SlugGlyph* glyph = &font_info.glyphs[i];
SlugGlyphBandData* band_data = &font_info.glyph_bands[i];
u32 glyph_curve_start = curve_start_indices[i];
u32 band_total = cast(u32)(band_data.band_counts.x + band_data.band_counts.y);
u32 next_band_index = band_index%FONT_TEX_WIDTH;
if(next_band_index+band_total > FONT_TEX_WIDTH)
{
band_index = cast(u32)((band_index/FONT_TEX_WIDTH+1) * FONT_TEX_WIDTH);
}
glyph.coords = U16Vec2(band_index%FONT_TEX_WIDTH, band_index/FONT_TEX_WIDTH);
u32 glyph_start = band_index;
SlugCurve[] curves = font_info.curves[glyph.curve_start .. glyph.curve_start+glyph.curve_count];
foreach(axis; 0 .. 2)
{
foreach(j; 0 .. band_data.band_counts[axis])
{
SlugSortBand(arena, &band_data.bands[axis][j], curves, axis);
}
}
u32 band_offset = band_total;
u32[] offsets = Alloc!(u32)(arena, band_total);
foreach(j; 0 .. band_data.band_counts.x)
{
offsets[j] = band_offset;
band_offset += band_data.bands[0][j].count;
}
foreach(j; 0 .. band_data.band_counts.y)
{
offsets[band_data.band_counts.x+j] = band_offset;
band_offset += band_data.bands[1][j].count;
}
foreach(j; 0 .. band_total)
{
u32 tl = glyph_start+j;
u32 di = cast(u32)((tl/FONT_TEX_WIDTH*FONT_TEX_WIDTH + tl%FONT_TEX_WIDTH) * 4);
u32 index = j;
u32 count = 0;
if(index < cast(u32)band_data.band_counts.x)
{
count = band_data.bands[0][index].count;
}
else
{
index -= band_data.band_counts.x;
count = band_data.bands[1][index].count;
}
band_texel.data[di .. di+2] = [count, offsets[j]];
}
foreach(j; 0 .. band_total)
{
u32 axis = j < band_data.band_counts.x ? 0 : 1;
u32 index = j < band_data.band_counts.x ? j : j - band_data.band_counts.x;
SlugBandEntry* band = &band_data.bands[axis][index];
u32 ls = glyph_start+offsets[j];
foreach(k; 0 .. band.count)
{
u32 ct = curve_start_indices[i] + band.indices[k]*2;
u32 tl = ls+k;
u32 di = cast(u32)((tl/FONT_TEX_WIDTH*FONT_TEX_WIDTH + tl%FONT_TEX_WIDTH) * 4);
band_texel.data[di .. di+2] = [ct%FONT_TEX_WIDTH, ct/FONT_TEX_WIDTH];
}
}
band_index = glyph_start+band_offset;
}
font_info.curve_texel = curve_texel;
font_info.band_texel = band_texel;
}
return result;
}
void
SlugSortBand(Arena* arena, SlugBandEntry* entry, SlugCurve[] curves, u32 axis)
{
if(entry.count <= 1) return;
SlugCurveRef[] refs = Alloc!(SlugCurveRef)(arena, entry.count);
foreach(i; 0 .. entry.count)
{
SlugCurve* curve = &curves[entry.indices[i]];
f32 mx = Max(curve.p1[axis], curve.p2[axis], curve.p3[axis]);
refs[i] = SlugCurveRef(entry.indices[i], mx);
}
sort!("a.sort_key < b.sort_key")(refs);
foreach(i; 0 .. entry.count)
{
entry.indices[i] = refs[i].index;
}
}
u32
SlugCountCurvesForGlyph(u32 glyph_index, stbtt_fontinfo* stb_font_info)
{
u32 result;
stbtt_vertex* vertices;
u32 vertex_count = stbtt_GetGlyphShape(stb_font_info, glyph_index, &vertices);
foreach(i; 0 .. vertex_count)
{
switch(vertices[i].type)
{
case STBTT_vline, STBTT_vcurve: result += 1; break;
case STBTT_vcubic: result += 2; break;
default: break;
}
}
if(vertex_count > 0)
{
stbtt_FreeShape(stb_font_info, vertices);
}
return result;
}
void
SlugBuildBands(Arena* arena, u32 glyph_index, SlugFontInfo* font_info, u32 band_count)
{
SlugGlyphBandData* band_data = &font_info.glyph_bands[glyph_index];
SlugGlyph* glyph = &font_info.glyphs[glyph_index];
Vec2 size = glyph.rect.max - glyph.rect.min;
band_data.band_counts = cast(u8)band_count;
for(u32 i = 0; i < band_count; i += 1)
{
band_data.bands[0][i].indices = Alloc!(u32)(arena, glyph.curve_count);
band_data.bands[1][i].indices = Alloc!(u32)(arena, glyph.curve_count);
}
glyph.max_bands = cast(u8)band_count;
glyph.band_scale = Vec2(
size.x > 0.0 ? cast(f32)(band_count)/size.x : 0.0,
size.y > 0.0 ? cast(f32)(band_count)/size.y : 0.0,
);
glyph.band_offset = -glyph.rect.min * glyph.band_scale;
for(u32 i = 0; i < glyph.curve_count; i += 1)
{
SlugCurve* curve = &font_info.curves[glyph.curve_start+i];
Vec2 curve_min = Vec2(
Min(curve.p1.x, curve.p2.x, curve.p3.x),
Min(curve.p1.y, curve.p2.y, curve.p3.y),
);
Vec2 curve_max = Vec2(
Max(curve.p1.x, curve.p2.x, curve.p3.x),
Min(curve.p1.y, curve.p2.y, curve.p3.y),
);
if(size.y > 0.0)
{
u32 b0 = cast(u32)clamp(floor(curve_min.y * glyph.band_scale.y + glyph.band_offset.y), 0.0, cast(f32)(band_count-1));
u32 b1 = cast(u32)clamp(floor(curve_max.y * glyph.band_scale.y + glyph.band_offset.y), 0.0, cast(f32)(band_count-1));
for(u32 j = b0; j <= b1; j += 1)
{
SlugBandEntry* entry = &band_data.bands[0][j];
entry.indices[entry.count++] = i;
}
}
if(size.x > 0.0)
{
u32 b0 = cast(u32)clamp(Floor(curve_min.x * glyph.band_scale.x + glyph.band_offset.x), 0.0, cast(f32)(band_count-1));
u32 b1 = cast(u32)clamp(Floor(curve_max.x * glyph.band_scale.x + glyph.band_offset.x), 0.0, cast(f32)(band_count-1));
for(u32 j = b0; j <= b1; j += 1)
{
SlugBandEntry* entry = &band_data.bands[1][j];
entry.indices[entry.count++] = i;
}
}
}
}