{"id":19535011,"url":"https://github.com/anthofoxo/fontengine","last_synced_at":"2025-04-26T14:35:38.361Z","repository":{"id":65364727,"uuid":"590757431","full_name":"anthofoxo/fontengine","owner":"anthofoxo","description":"An api agnostic text rendering engine using signed distance fields.","archived":true,"fork":false,"pushed_at":"2023-12-16T06:32:42.000Z","size":138,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-14T12:13:00.179Z","etag":null,"topics":["font","sdf","text"],"latest_commit_sha":null,"homepage":"https://anthofoxo.xyz","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anthofoxo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-01-19T06:02:02.000Z","updated_at":"2024-07-21T01:28:53.000Z","dependencies_parsed_at":"2023-12-15T06:50:41.779Z","dependency_job_id":null,"html_url":"https://github.com/anthofoxo/fontengine","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anthofoxo%2Ffontengine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anthofoxo%2Ffontengine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anthofoxo%2Ffontengine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anthofoxo%2Ffontengine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anthofoxo","download_url":"https://codeload.github.com/anthofoxo/fontengine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251001411,"owners_count":21520942,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["font","sdf","text"],"created_at":"2024-11-11T02:16:52.854Z","updated_at":"2025-04-26T14:35:38.037Z","avatar_url":"https://github.com/anthofoxo.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"!! UPDATE !! Library will be updated to use c++! the api however will remain c style !!\n\n!! Warning, a newer more spohisticated version of this is under development, improving glyph rendering and multilingual support !!\n\n!! Under active development, api *may* change, readme may not necessarily be up to date !!\n\n!! Todo: Move documentation into github wiki pages\n\nSample text rendered from the engine with OpenGL as the backend\n![image](sample.png)\n\n# Antho Foxo's Font Engine Overview\nThis is an operating system and rendering api agnostic text rendering engine. We are using signed distance fields (sdfs) bitmaps inside a texture cache to achive scalable rendering at any font size without having to cache multiple sizes of each glyph. As text is drawn it'll populate the texture cache as needed.\n\n### Contributor list\nAnthoFoxo\n\n## Dependencies\nDependencies are kept at a minimum, only requiring the following:\n* [stb_truetype.h](https://github.com/nothings/stb/blob/master/stb_truetype.h)\n* [stb_rect_pack.h](https://github.com/nothings/stb/blob/master/stb_rect_pack.h)\n* c std lib: malloc, realloc, free, memset, memcpy, strlen\n\nThese dependencies are expected to be setup before including this library.\n\n\n`implementation.c`\n```c\n#define STB_RECT_PACK_IMPLEMENTATION\n#define STB_TRUETYPE_IMPLEMENTATION\n#define AFFE_IMPLEMENTATION\n#include \"stb_rect_pack.h\"\n#include \"stb_truetype.h\"\n#include \"af_fontengine.h\"\n```\n\n`main.c`\n```c\n// Assume this is always included in the examples\n#include \"af_fontengine.h\"\n\nint main()\n{\n    // use public api functions\n}\n```\n\n## Error Safety (Based on C++ exception safety)\n\nAll API functions provide some level of error safety, that is if an error occurs, you have some guarentee that it doesn't break the application.\nAPI functions which invoke user callbacks are not taken into consideration when determining error safety.\nAll API functions have the end goal of having strong error guarentee.\n\n- Nofail error guarantee - The function never causes errors. Nofail errors are reported by other means or concealed.\n- Strong error guarantee - If the function causes an error, the state of the program is rolled back to the state just before the function call.\n- Basic error guarantee - If the function causes an error, the program is in a valid state. No resources are leaked, and all objects' invariants are intact.\n- No error guarantee - If the function causes an error, the program may not be in a valid state: resource leaks, memory corruption, or other invariant-destroying errors may have occurred.\n\n## API Convention\n* Macros are prefixed with `AFFE_` with exception of `NULL`, `FALSE`, and `TRUE`\n* Functions are prefixed with `affe_`\n* Implmentation specific functions are prefixed with `affe__`\n* Functions always take `affe_context*` as their first parameter, with exception of `affe_context_create`\n* User callbacks always have the user pointer passed as the second parameter\n\n# Creating and deleting the context\nAll state is contained inside a context. To create a context, you must fill out `affe_context_create_info` which tells the the engine various important details to be usable. When you are done, make sure to delete it.\n\n```c\nint main()\n{\n    affe_context_create_info info;\n    memset(\u0026info, 0, sizeof(affe_context_create_info));\n\n    // Width and height of the texture cache\n    info.width = 512;\n    info.height = 512;\n\n    // The proc functions will be called throughout the lifetime of the engine\n    // Your rendering implmentation is defined by these functions\n    // The user pointer allows you to use custom data for your implmentation\n    info.user_ptr = NULL;\n    info.create_proc = NULL;\n    info.update_proc = NULL;\n    info.draw_proc = NULL;\n    info.delete_proc = NULL;\n    \n    // Rasterization settings (more about these later)\n    info.edge_value = 0.8f;\n    info.padding = 8;\n    info.size = 48;\n\n    // Tell the engine how much space to allocate for the buffer (This is not a byte count)\n    info.buffer_quad_count = 256;\n\n    // Create the context with all the settings\n    affe_context* ctx = affe_context_create(\u0026info);\n\n    // Context failed to create\n    if(!ctx) return 1;\n\n    // Use the api!\n\n    // After you're done, delete the context\n    affe_context_delete(ctx);\n}\n```\n\n# Loading fonts\nThe engine provides no way to do file io, you must load font files (ttf/otf) into a memory buffer and provide that to use fonts.\n\n```c\n// ^^^ Context is created ^^^\n\n// Font data must stay allocated during operation of the engine\n// The data pointer must not change during operation\n\n// Notice, current security issue in stb_truetype\n// Only use trusted fonts, no range checking is done\nvoid* data = loadfile(\"font.ttf\");\n\n// Font files can contain more than one font\n// Use 0 if unsure                  v\nint font = affe_font_add(ctx, data, 0, TRUE);\n//                                     ^^^^\n// The engine can take ownership of the data pointer for you\n// Set to true to give the engine ownership of the data, false otherwise\n\n// If font is `AFFE_INVALID` then the font failed to be added\n// After a font is loaded you can set it as the current font\naffe_set_font(ctx, font);\n\n// vvv Context is deleted vvv\n```\n\n# Drawing text\nDrawing text is pretty simple, the easiest method is to call `affe_text_draw`.\nAll text is expected to be UTF8 encoded.\n\n```c\n// ^^^ Context is created and a font loaded and set ^^^\nconst char* text = \"Hi mom!\";\n\n// This specifies where the draw origin is, in viewport space\n//                  vvv  vvv  \naffe_text_draw(ctx, 100, 100, text, NULL);\n//                                  ^^^^\n// This specifies where the text should stop rendering,\n// If null `strlen` will be used to calculate this for you.\n\n// `affe_text_draw_inline` may be used to draw text while ignoring line breaks for performance increase\n\n// vvv Context is deleted vvv\n```\n\nThis is enough for the engine to submit the proper calls to draw this text.\nHowever we did not hook up and of the proc functions so nothing is visible.\nOpenGL will be used to demonstrate implmentation.\n\n# Rendering implementation / OpenGL\nThe engine alone makes no calls to any graphics api for you. You must provide this yourself. To do this you set the `xxx_proc` functions in the `affe_context_create_info` struct.\n\nThis example is written with modern opengl 3\n\n```c\n\nstruct user_data\n{\n    unsigned int texture;\n    unsigned int vao, vbo;\n    unsigned int program;\n}\n\ntypedef struct user_data user_data;\n\nstatic int create_proc(affe_context* ctx, void* user_ptr, int w, int h)\n{\n    // Create an opengl texture with the specified size\n    // Pixel data will later be provided in parts to update the image\n    // Setup the format to use one channel unsigned bytes\n    // internalFormat = R8, format = GL_RED, type = GL_UNSIGNED_BYTE\n\n    // Create a vertex array and an array buffer,\n    // The buffer size is based on the number of quads specified during context creation\n    // This size in bytes can be retrieved by `affe_buffer_size`\n    // The buffer should be setup to use GL_STREAM_DRAW\n    \n    // The buffer will have three attributes stride = sizeof(affe_vertex)\n    // 2 floats for xy position;             offset = (const void*)offsetof(affe_vertex, x)\n    // 2 floats for st texture coordinates   offset = (const void*)offsetof(affe_vertex, s)\n    // 4 floats for rgba color               offset = (const void*)offsetof(affe_vertex, r)\n\n    // Create a vertex and fragment shader, compile and link them into a program\n\n    // An example shader will be provided later\n\t\n    // Return `TRUE` if everything was successful, `FALSE` otherwise\n\treturn TRUE;\n}\n\nstatic void delete_proc(affe_context* ctx, void* user_ptr)\n{\n    // Delete the texture, vertex array, vertex buffer, and shader program\n}\n\nstatic void update_proc(affe_context* ctx, void* user_ptr, int x, int y, int w, int h, void* pixels)\n{\n    // An offset and size is provided to you along with pixel data to modify the texture\n\n    // Bind your texture and call subimage\n\n    // !! NOTICE !!\n    // A common pitfall with updating the texture is row alignment\n    // The engine does not align your pixel data\n    \n    // Set the row alignment to 1 to successfully upload the data\n    // glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\n    // Restore it back to default after upload if needed\n    // glPixelStorei(GL_UNPACK_ALIGNMENT, 4);\n}\n\nstatic void draw_proc(affe_context* ctx, void* user_ptr, affe_vertex* verts, long long verts_count)\n{\n    // The vertex is provided along with the number of verticies\n    // This is in an interleaved format\n\n    // Vertex positions are provided in viewport space,\n    // you may want to iterate over the vertices here\n    // to put them into normalized device space. (It's okay to modify them)\n    // Alternativly you can use an orthographic projection matrix in the shader\n\n    // Bind the array buffer and use subdata to update the data store; see invalidation below\n    // Make sure your texture, vertex array, and program are bound as well\n\n    // You'll want alpha blending enabled as well for good looking text\n\n    // Finally submit the draw call\n\n    // -- Invalidation --\n    // Since the array buffer is reused over and over\n    // you can take advantage of buffer invalidation to potentially increase render times\n    // without opengl 4+ or extensions you must use `glBufferData` to do this\n    // Call `glBufferData` using the same, exact same size, and usage hint, with NULL as the data\n    // After invalidation you can use subdata to put data into the buffer\n}\n\nstatic affe_context* make_context(user_data* impl_data)\n{\n    affe_context_create_info info;\n    memset(\u0026info, 0, sizeof(affe_context_create_info));\n    info.width = 512;\n    info.height = 512;\n\n    // Set the user pointer so we can access our data\n    info.user_ptr = impl_data;\n\n    // This function is called once when creating the context\n    // A perfect time to create the texture, buffers, and shader program\n    info.create_proc = \u0026create_proc;\n\n    // Called when the context is being deleted\n    info.delete_proc = \u0026delete_proc;\n\n    // Called when a new glyph is being inserted into the texture cache\n    info.update_proc = \u0026update_proc;\n\n    // Called when text is ready to be displayed\n    info.draw_proc = \u0026draw_proc;\n    \n    // How many quads to allocate for our buffer?\n    info.buffer_quad_count = 256;\n\n    // Rasterization settings\n    info.edge_value = 0.8f;\n    info.padding = 8;\n    info.size = 48;\n}\n\nint main()\n{\n    // Using whatever api you like, create a window and an opengl context\n    // Then make it current on whatver you'll be using for your rendering thread\n    // I'd personally suggest glfw3 and glad2\n\n    user_data data;\n\n    affe_context* ctx = make_context(\u0026data);\n\n    // Load a font into memory and set it for use in the library\n\n    // Start main game loop\n\n    const char* text = \"Oh hey there, what's going on?\";\n    affe_text_draw(ctx, 100, 100, text, NULL);\n\n    // End main game loop\n\n    affe_context_delete(ctx);\n}\n```\n\n# How to write the shaders, text is blurry\nDue to the nature of how sdfs work, you cannot just simply output the texture sample.\nThe sample given represents how far from the glyph edge you are.\n\nAdditionally, doing some super/multisampling will greatly improve the looks of the text.\n\n`text.vert.glsl`\n```glsl\n#version 330 core\n\nlayout(location = 0) in vec2 vert_pos;\nlayout(location = 1) in vec2 vert_tex;\nlayout(location = 2) in vec4 vert_col;\n\nout vec2 frag_tex;\nout vec4 frag_col;\n\n\n// Simple vertex shader, set the output and forward the information\nvoid main(void)\n{\n    // You will need to apply a transformation to vert_pos if it wasn't done in c\n    gl_Position = vec4(vert_pos, 0.0, 1.0);\n    frag_tex = vert_tex;\n    frag_col = vert_col;\n}\n```\n\n`text.frag.glsl`\n```glsl\n#version 330 core\n\nin vec2 frag_tex;\nin vec4 frag_col;\n\nlayout(location = 0) out vec4 out_col;\n\nuniform sampler2D u_sampler;\n\n// This is the same value as in the c code,\nconst float onedge_value = 0.8;\n\nvoid main(void)\n{\n    // Set output color, edge of the glyph is controlled by alpha\n    out_col = frag_col;\n\n    // Sample the texture to find out how far from the edge we are\n    // when `dist \u003e onedge_value`  you are inside the glyph\n    // otherwise you are outside the glyph\n    // Higher on edge values give better outlines\n    float dist = texture(u_sampler, frag_tex).r;\n\n    // Gets the sum of the absolute derivatives in x and y\n    // using local differencing for the input argument\n    float w = fwidth(dist);\n\n    // Use this value to smooth out a tiny edge,\n    // this will automatically scale nicely for any size text\n    out_col.a *= smoothstep(onedge_value - w, onedge_value + w, dist);\n}\n```\n# Fine tuning rasterization settings\nThe three main rasterizer settings are `edge_value`, `padding`, and `size`\n* **size** - controls at what size the sdf will be created, unrelated to font size. The higher the better looking text you'll get, at the cost of texture cache space. 48 is a good default.\n* **padding** - controls how much space is put around the glyph, this is needed to actually store the distance field. The heigher the better edges can look, at the cost of texture size, 8 is a good default.\n* **edge_value** - controls at what value is considered the \"edge\" of the glyph, in shaders this is used to discard or blend away stuff too far away. The higher this is, the better represented the distance fields are. 0.8 is a good default.\n\n# State management\nThe engine has a concept of a state stack. Where you can change rendering settings and push/pop for later.\n\n```c\n// the current state now has red as the color\naffe_set_color(ctx, 1, 0, 0, 1);\n\n// You can push this into the stack to freely modify the state\n// without worrying about losing old values\naffe_state_push(ctx);\n\n// Change some state, color is now yellow\naffe_set_color(ctx, 1, 1, 0, 1);\n\n// Pop the state off the stack, old state is restored\n// Color is now red again\naffe_state_pop(ctx);\n\n// If you ever want default state values\naffe_state_clear(ctx);\n```\n\n# Changing state\nState includes color, current font, styling etc.\n\n```c\n// horizontal alignment AFFE_ALIGN_LEFT, AFFE_ALIGN_RIGHT, AFFE_ALIGN_RIGHT\n\naffe_set_color(ctx, r, g, b, a); // rgba, engine does not clip these (0-1)\naffe_set_font(ctx, font);\naffe_set_size(ctx, size); // Font size in pixels\naffe_set_alignment(ctx, AFFE_ALIGN_CENTER); // The cursor point is now where text will be centered on\n```\n\n# Font fallbacks\nAfter fonts are loaded you can set fonts up as a fallback for others.\nFor example, if you font thats currently set doesn't contain a glyph. It'll look through its' fallbacks to try finding one. No fallbacks are setup by default.\n\n```c\nint main_font = affe_font_add(...);\nint fallback_font = affe_font_add(...);\n// return `TRUE` on success, `FALSE` otherwise\nint success = affe_font_fallback(ctx, main_font, fallback_font);\n\naffe_set_font(ctx, main_font);\n\n// If this succeeds, when the main font is used and a glyph cannot be found\n// It'll look at the fallbacks to try finding a glyph to use\n```\n\n# Engine flags\nThe `affe_context_create_info` has a flags field which is reserved for later.\n\n# Error handling\nThere are a few possible errors, some functions can return `NULL` or `AFFE_INVALID` as an error.\nOther errors are handled using the `error_proc` inside `affe_context_create_info`.\n\nWhen stack overflow or underflow errors occur, an error is reported but the engine remains in usable state.\n\nWhen the cache fails to find a location for a glyph, the glyph will just not render. Theres no current way to fix this other than to recreate the context with a larger cache.\n\n# Known bugs\n* Text rendering ignores control characters, rendering as a small box.\n* Some international glyphs do not rasterize correctly.\n\n# Buffer flush behavior / buffer flush control\n\nSince v0.1.4 you can have more control over how the vertex buffer gets flushed. Flushing the buffer simply invokes the user draw callback with the generated vertex information.\n\nTo change the buffer behavior, call `affe_buffer_flush_behavior` with one of these constants.\n\n`affe_buffer_flush` may be called to manually flush the buffer, this is pointless to do on default buffer control.\n\n`AFFE_BUFFER_FLUSH_CONTROL_AUTOMATIC` :\nDefault behavior, the buffer is flushed at the end of text draws.\n\n`AFFE_BUFFER_FLUSH_CONTROL_NONE` :\nFlushing is not done at the end of draw calls, must manually invoke at the end of frame. Note: Buffer flush will still be invoked when the buffer is filled \n\nTo manually flush the buffer, call `affe_buffer_flush`.\n\n# Planned features\n* Font kerning\n* Cache resizing\n* Cache clearing\n* Veritcal text alignment\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanthofoxo%2Ffontengine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanthofoxo%2Ffontengine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanthofoxo%2Ffontengine/lists"}