summaryrefslogtreecommitdiff
path: root/text.c
diff options
context:
space:
mode:
authorLeo Tenenbaum <pommicket@gmail.com>2020-11-23 18:29:58 -0500
committerLeo Tenenbaum <pommicket@gmail.com>2020-11-23 18:29:58 -0500
commit55d0ece0a9072ca409bdf6ff2f3b6d0b268e2952 (patch)
tree5df0cbfacf3d9442ddb34fdf5fb3b59f4e74d82e /text.c
parent5e458dff3bcc832b0b28d83bd3ef482174d1dc09 (diff)
unicode text rendering working
Diffstat (limited to 'text.c')
-rw-r--r--text.c230
1 files changed, 182 insertions, 48 deletions
diff --git a/text.c b/text.c
index 7007ad6..5bc0bac 100644
--- a/text.c
+++ b/text.c
@@ -3,25 +3,34 @@
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTT_STATIC
no_warn_start
-#include "stb_truetype.h"
+#include "lib/stb_truetype.h"
no_warn_end
#include <stdarg.h>
#include <stdlib.h>
#include <GL/gl.h>
+#define UNICODE_CODE_POINTS 0x110000 // number of Unicode code points
+// We split up code points into a bunch of pages, so we don't have to load all of the font at
+// once into one texture.
+#define CHAR_PAGE_SIZE 2048
+#define CHAR_PAGE_COUNT UNICODE_CODE_POINTS / CHAR_PAGE_SIZE
+
struct Font {
float char_height;
- GLuint texture;
- u32 nchars;
- stbtt_bakedchar chars[];
+ GLuint textures[CHAR_PAGE_COUNT];
+ int tex_widths[CHAR_PAGE_COUNT], tex_heights[CHAR_PAGE_COUNT];
+ stbtt_bakedchar *char_pages[CHAR_PAGE_COUNT]; // character pages. NULL if the page hasn't been loaded yet.
+ // TTF data (i.e. the contents of the TTF file)
+ u8 *ttf_data;
+ int curr_page;
};
static char text_err[200];
-static void text_clear_err(void) {
+void text_clear_err(void) {
text_err[0] = '\0';
}
-static bool text_has_err(void) {
+bool text_has_err(void) {
return text_err[0] != '\0';
}
@@ -38,9 +47,57 @@ static void text_set_err(char const *fmt, ...) {
}
}
+static void text_load_char_page(Font *font, int page) {
+ if (font->char_pages[page]) {
+ // already loaded
+ return;
+ }
+ font->char_pages[page] = calloc(CHAR_PAGE_SIZE, sizeof *font->char_pages[page]);
+ for (int bitmap_width = 128, bitmap_height = 128; bitmap_width <= 4096; bitmap_width *= 2, bitmap_height *= 2) {
+ u8 *bitmap = calloc((size_t)bitmap_width, (size_t)bitmap_height);
+ if (bitmap) {
+ int err = stbtt_BakeFontBitmap(font->ttf_data, 0, font->char_height, bitmap,
+ bitmap_width, bitmap_height, page * CHAR_PAGE_SIZE, CHAR_PAGE_SIZE, font->char_pages[page]);
+ if (err > 0) {
+ // font converted to bitmap successfully.
+ GLuint texture = 0;
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap_width, bitmap_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ #if DEBUG
+ debug_println("Loaded font page %d with %dx%d bitmap as texture %u.", page, bitmap_width, bitmap_height, texture);
+ #endif
+ font->textures[page] = texture;
+ font->tex_widths[page] = bitmap_width;
+ font->tex_heights[page] = bitmap_height;
+ GLenum glerr = glGetError();
+ if (glerr) {
+ text_set_err("Couldn't create texture for font (GL error %u).", glerr);
+ if (texture) glDeleteTextures(1, &texture);
+ break;
+ }
+ }
+ } else {
+ text_set_err("Not enough memory for font bitmap.");
+ }
+ free(bitmap);
+ if (font->textures[page]) { // if font loaded successfully
+ break;
+ }
+ }
+ if (!font->textures[page] && !text_has_err()) {
+ text_set_err("Couldn't convert font to bitmap.");
+ }
+ if (text_has_err()) {
+ free(font->char_pages[page]);
+ font->char_pages[page] = NULL;
+ }
+}
+
Font *text_font_load(char const *ttf_filename, float font_size) {
Font *font = NULL;
- u32 nchars = 128;
FILE *ttf_file = fopen(ttf_filename, "rb");
text_clear_err();
@@ -48,54 +105,25 @@ Font *text_font_load(char const *ttf_filename, float font_size) {
if (ttf_file) {
fseek(ttf_file, 0, SEEK_END);
u32 file_size = (u32)ftell(ttf_file);
+ fseek(ttf_file, 0, SEEK_SET);
if (file_size < (50UL<<20)) { // fonts aren't usually bigger than 50 MB
u8 *file_data = calloc(1, file_size);
- font = calloc(1, sizeof *font + nchars * sizeof *font->chars);
+ font = calloc(1, sizeof *font);
if (file_data && font) {
- if (fread(file_data, 1, file_size, ttf_file) == file_size) {
- font->nchars = nchars;
+ size_t bytes_read = fread(file_data, 1, file_size, ttf_file);
+ if (bytes_read == file_size) {
font->char_height = font_size;
-
- for (int bitmap_width = 256, bitmap_height = 256; bitmap_width <= 4096; bitmap_width *= 2, bitmap_height *= 2) {
- u8 *bitmap = calloc((size_t)bitmap_width, (size_t)bitmap_height);
- if (bitmap) {
- int err = stbtt_BakeFontBitmap(file_data, 0, font->char_height, bitmap,
- bitmap_width, bitmap_height, 0, (int)font->nchars, font->chars);
- if (err > 0) {
- // font converted to bitmap successfully.
- GLuint texture = 0;
- glGenTextures(1, &texture);
- glBindTexture(GL_TEXTURE_2D, texture);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap_width, bitmap_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- #if DEBUG
- debug_println("Loaded font %s with %dx%d bitmap.", ttf_filename, bitmap_width, bitmap_height);
- #endif
- font->texture = texture;
- if (glGetError()) {
- text_set_err("Couldn't create texture for font.");
- }
- }
- } else {
- text_set_err("Not enough memory for font bitmap.");
- }
- free(bitmap);
- if (font->texture) { // if font loaded successfully
- break;
- }
- }
- if (!font->texture && !text_has_err()) {
- text_set_err("Couldn't convert font to bitmap.");
- }
+ font->ttf_data = file_data;
+ text_load_char_page(font, 0); // load page with Latin text, etc.
+ font->curr_page = -1;
} else {
- text_set_err("Couldn't read font file.", ttf_filename);
+ text_set_err("Couldn't read font file.");
}
} else {
text_set_err("Not enough memory for font.");
}
- free(file_data);
if (text_has_err()) {
+ free(file_data);
free(font);
font = NULL;
}
@@ -109,11 +137,117 @@ Font *text_font_load(char const *ttf_filename, float font_size) {
return font;
}
-#if 0
+typedef struct {
+ float x, y;
+} TextRenderState;
+
+static void text_render_with_page(Font *font, int page) {
+ if (font->curr_page != page) {
+ if (font->curr_page != -1) {
+ // we were rendering chars from another page.
+ glEnd(); // stop doing that
+ }
+ text_load_char_page(font, page); // load the page if necessary
+ glEnable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, font->textures[page]);
+ glBegin(GL_QUADS);
+ font->curr_page = page;
+ }
+}
+
+void text_chars_begin(Font *font) {
+ text_render_with_page(font, 0); // start by loading Latin text
+}
+
+void text_chars_end(Font *font) {
+ glEnd();
+ glDisable(GL_TEXTURE_2D);
+ font->curr_page = -1;
+}
+
+static void text_render_char_internal(Font *font, char32_t c, TextRenderState *state) {
+ if (c >= 0x30000 && c < 0xE0000){
+ // these Unicode code points are currently unassigned. replace them with ☐.
+ // (specifically, we don't want to use extra memory for pages which
+ // won't even have any valid characters in them)
+ c = 0x2610;
+ }
+ if (c >= UNICODE_CODE_POINTS) c = 0x2610; // code points this big should never appear in valid Unicode
+ uint page = c / CHAR_PAGE_SIZE;
+ uint index = c % CHAR_PAGE_SIZE;
+ text_render_with_page(font, (int)page);
+ stbtt_bakedchar *char_data = font->char_pages[page];
+ if (char_data) { // if page was successfully loaded
+ stbtt_aligned_quad q = {0};
+ // because stb_truetype uses down is positive, we need to negate the y
+ // coordinate, pass it into the function, then negate it back.
+ state->y = -state->y;
+ stbtt_GetBakedQuad(char_data, font->tex_widths[page], font->tex_heights[page],
+ (int)index, &state->x, &state->y, &q, 1);
+ state->y = -state->y;
+ glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,-q.y1);
+ glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,-q.y1);
+ glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,-q.y0);
+ glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,-q.y0);
+ }
+}
+
+void text_render_char(Font *font, char32_t c, float *x, float *y) {
+ TextRenderState state = {*x, *y};
+ text_render_char_internal(font, c, &state);
+ *x = state.x;
+ *y = state.y;
+}
+
+static void text_render_internal(Font *font, char const *text, float *x, float *y) {
+ mbstate_t mbstate = {0};
+ TextRenderState render_state = {*x, *y};
+ text_chars_begin(font);
+ char32_t c = 0;
+ char const *end = text + strlen(text);
+ while (text != end) {
+ size_t ret = mbrtoc32(&c, text, (size_t)(end - text), &mbstate);
+ if (ret == 0) break;
+ if (ret == (size_t)(-2)) { // incomplete multi-byte character
+ text_render_char_internal(font, '?', &render_state);
+ text = end; // done reading text
+ } else if (ret == (size_t)(-1)) {
+ // invalid UTF-8; skip this byte
+ text_render_char_internal(font, '?', &render_state);
+ ++text;
+ } else {
+ if (ret != (size_t)(-3))
+ text += ret; // character consists of `ret` bytes
+ switch (c) {
+ default:
+ text_render_char_internal(font, (char32_t)c, &render_state);
+ break;
+ }
+ }
+ }
+ text_chars_end(font);
+ *x = render_state.x;
+ *y = render_state.y;
+}
+
void text_render(Font *font, char const *text, float x, float y) {
-
+ text_render_internal(font, text, &x, &y);
}
void text_get_size(Font *font, char const *text, float *width, float *height) {
+ float x = 0, y = 0;
+ text_render_internal(font, text, &x, &y);
+ if (width) *width = x;
+ if (height) *height = y + font->char_height * (2/3.0f);
+}
+
+void text_font_free(Font *font) {
+ free(font->ttf_data);
+ stbtt_bakedchar **char_pages = font->char_pages;
+ for (int i = 0; i < CHAR_PAGE_COUNT; ++i) {
+ if (char_pages[i]) {
+ free(char_pages[i]);
+ }
+ }
+ free(font);
}
-#endif