// SPDX-License-Identifier: Zlib OR MIT // // Copyright (c) 2024 Antonio Niño Díaz #include #include #include #include #include // BMFont format structures // ------------------------ typedef struct __attribute__((packed)) { uint8_t magic[3]; // "BMF" uint8_t version; // 3 } bmf_header; typedef struct __attribute__((packed)) { uint8_t type; // 1, 2, 3, 4 or 5 uint8_t size[4]; // Size of the block uint8_t data[]; } bmf_block_header; typedef struct __attribute__((packed)) { uint16_t line_height; // Distance in pixels between each line of text. uint16_t base; // Number of pixels from the top of the line to the base of the characters. uint16_t scale_w; uint16_t scale_h; uint16_t pages; uint8_t bit_field; uint8_t alpha_channel; uint8_t red_channel; uint8_t green_channel; uint8_t blue_channel; } bmf_block_2_common; // The number of characters in one file can be calculated by calculating the // result of "size / sizeof(bmf_block_4_char)" typedef struct __attribute__((packed)) { uint32_t id; uint16_t x; uint16_t y; uint16_t width; uint16_t height; int16_t xoffset; int16_t yoffset; int16_t xadvance; uint8_t page; uint8_t channel; } bmf_block_4_char; typedef struct __attribute__((packed)) { uint32_t first; uint32_t second; uint16_t amount; } bmf_block_5_kerning_pair; // Internal data structures // ------------------------ typedef bmf_block_4_char block_char; typedef struct { uint32_t first; uint32_t second; int16_t amount; } kerning_pair; typedef struct { uint16_t line_height; uint16_t base; size_t num_chars; block_char *chars; size_t num_kernings; kerning_pair *kernings; // Variables used for the current printing context int16_t pointer_x; int16_t pointer_y; int16_t box_left; int16_t box_top; uint32_t last_codepoint; // If the font includes a replacement character glyph, its pointer will be // saved here for ease of access. const block_char *replacement_character; } dsf_font_internal_state; // Functions // --------- static int DSF_block_char_cmp(const void *a, const void *b) { const block_char *a_ = a; const block_char *b_ = b; return a_->id - b_->id; } static int DSF_kerning_pair_cmp(const void *a, const void *b) { const kerning_pair *a_ = a; const kerning_pair *b_ = b; // Compare the first codepoint. If it matches, compare the second codepoint. if (a_->first != b_->first) return a_->first - b_->first; return a_->second - b_->second; } static const block_char *DSF_CodepointFindGlyph(dsf_handle handle, uint32_t codepoint) { dsf_font_internal_state *font = (dsf_font_internal_state *)handle; // Codepoint to find block_char key = { 0 }; key.id = codepoint; const block_char *ch = bsearch(&key, font->chars, font->num_chars, sizeof(block_char), DSF_block_char_cmp); if (ch == NULL) return font->replacement_character; return ch; } static dsf_error DSF_LoadFile(const char *path, void **data, size_t *_size) { FILE *f = fopen(path, "rb"); if (f == NULL) return DSF_FILE_OPEN_ERROR; if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return DSF_FILE_SEEK_ERROR; } size_t size = ftell(f); if (size == 0) { fclose(f); return DSF_FILE_EMPTY; } rewind(f); char *buffer = malloc(size); if (buffer == NULL) { fclose(f); return DSF_NO_MEMORY; } if (fread(buffer, 1, size, f) != size) { fclose(f); return DSF_FILE_READ_ERROR; } fclose(f); *_size = size; *data = buffer; return DSF_NO_ERROR; } dsf_error DSF_LoadFontMemory(dsf_handle *handle, const void *data, int32_t data_size) { dsf_error ret = DSF_NO_ERROR; const uint8_t *ptr = data; // Basic checks if ((handle == NULL) || (data == NULL) || (data_size <= 0)) return DSF_INVALID_ARGUMENT; // Read header const bmf_header *header = (const bmf_header *)ptr; if (!((header->magic[0] == 'B') && (header->magic[1] == 'M') && (header->magic[2] == 'F'))) return DSF_BAD_MAGIC; if (header->version != 3) return DSF_BAD_VERSION; ptr += sizeof(bmf_header); data_size -= sizeof(bmf_header); // Allocate space for the handle dsf_font_internal_state *font = calloc(1, sizeof(dsf_font_internal_state)); if (font == NULL) return DSF_NO_MEMORY; *handle = (dsf_handle)font; // Return handle to user // Read blocks while (1) { const bmf_block_header *block_header = (const bmf_block_header *)ptr; uint8_t type = block_header->type; uint32_t block_size = ((uint32_t)block_header->size[0]) | ((uint32_t)block_header->size[1] << 8) | ((uint32_t)block_header->size[2] << 16) | ((uint32_t)block_header->size[3] << 24); const uint8_t *data = ptr + 1 + 4; if (type == 2) { // Ensure the size is correct if (block_size != sizeof(bmf_block_2_common)) { ret = DSF_BAD_CHUNK_SIZE; goto error; } bmf_block_2_common block_common; memcpy(&block_common, data, block_size); font->line_height = block_common.line_height; font->base = block_common.base; } else if (type == 4) { // Ensure the total size is a multiple of the block size if (block_size % sizeof(bmf_block_4_char) != 0) { ret = DSF_BAD_CHUNK_SIZE; goto error; } font->num_chars = block_size / sizeof(bmf_block_4_char); font->chars = calloc(font->num_chars, sizeof(block_char)); if (font->chars == NULL) { ret = DSF_NO_MEMORY; goto error; } memcpy(font->chars, data, block_size); qsort(font->chars, font->num_chars, sizeof(block_char), DSF_block_char_cmp); } else if (type == 5) { // Ensure the total size is a multiple of the block size if (block_size % sizeof(bmf_block_5_kerning_pair) != 0) { ret = DSF_BAD_CHUNK_SIZE; goto error; } font->num_kernings = block_size / sizeof(bmf_block_5_kerning_pair); font->kernings = calloc(font->num_kernings, sizeof(kerning_pair)); if (font->kernings == NULL) { ret = DSF_NO_MEMORY; goto error; } const uint8_t *src = data; uint8_t *dst = (uint8_t *)font->kernings; for (size_t i = 0; i < font->num_kernings; i++) { memcpy(dst, src, sizeof(bmf_block_5_kerning_pair)); dst += sizeof(kerning_pair); src += sizeof(bmf_block_5_kerning_pair); } qsort(font->kernings, font->num_kernings, sizeof(kerning_pair), DSF_kerning_pair_cmp); } ptr += block_size + 1 + 4; data_size -= block_size + 1 + 4; if (data_size == 0) break; if (data_size < 0) { ret = DSF_UNEXPECTED_END; goto error; } } if (font->num_chars == 0) { ret = DSF_NO_CHARACTERS; goto error; } // Look for a replacement character glyph in the font. // Initialize the value to NULL so that DSF_CodepointFindGlyph() returns // NULL if no replacement character glyph is found. font->replacement_character = NULL; font->replacement_character = DSF_CodepointFindGlyph(*handle, REPLACEMENT_CHARACTER); return DSF_NO_ERROR; error: free(font->chars); free(font->kernings); free(font); return ret; } dsf_error DSF_FreeFont(dsf_handle *handle) { if (handle == NULL) return DSF_INVALID_ARGUMENT; dsf_font_internal_state *font = (dsf_font_internal_state *)*handle; free(font->chars); free(font->kernings); free(font); *handle = 0; return DSF_NO_ERROR; } dsf_error DSF_LoadFontFilesystem(dsf_handle *handle, const char *path) { if ((handle == NULL) || (path == NULL)) return DSF_INVALID_ARGUMENT; size_t size; void *data; dsf_error error = DSF_LoadFile(path, &data, &size); if (error != DSF_NO_ERROR) return error; error = DSF_LoadFontMemory(handle, data, size); free(data); return error; } static size_t DSF_UTF8_CodepointRead(const char *str, uint32_t *codepoint) { // https://en.wikipedia.org/wiki/UTF-8#Encoding size_t size; uint32_t rune; uint32_t b1 = str[0]; if ((b1 & 0x80) == 0) { size = 1; *codepoint = b1 & 0x7F; return 1; } else if ((b1 & 0xE0) == 0xC0) { size = 2; rune = b1 & 0x1F; } else if ((b1 & 0xF0) == 0xE0) { size = 3; rune = b1 & 0x0F; } else if ((b1 & 0xF8) == 0xF0) { size = 4; rune = b1 & 0x07; } else { goto error; } for (size_t i = 1; i < size; i++) { uint32_t b = str[i]; if ((b & 0xC0) != 0x80) goto error; rune <<= 6; rune |= b & 0x3F; } *codepoint = rune; return size; error: // Incorrect encoding. Advance characters until we find one character // that isn't a continuation character. size = 1; str++; while (1) { uint8_t c = *str++; size++; if ((c & 0xC0) != 0x80) break; } *codepoint = REPLACEMENT_CHARACTER; return size; } dsf_error DSF_CodepointRenderDryRun(dsf_handle handle, uint32_t codepoint) { if ((handle == 0) || (codepoint == 0)) return DSF_INVALID_ARGUMENT; dsf_font_internal_state *font = (dsf_font_internal_state *)handle; if (codepoint == '\n') { font->pointer_x = font->box_left; font->pointer_y += font->line_height; return DSF_NO_ERROR; } const block_char *ch = DSF_CodepointFindGlyph(handle, codepoint); if (ch == NULL) return DSF_CODEPOINT_NOT_FOUND; int x1 = font->pointer_x; int x2 = x1 + ch->width; int y1 = font->pointer_y; int y2 = y1 + ch->height; x1 += ch->xoffset; x2 += ch->xoffset; y1 += ch->yoffset; y2 += ch->yoffset; font->pointer_x += ch->xadvance; // Kerning pair to find kerning_pair key = { 0 }; key.first = font->last_codepoint; key.second = codepoint; const kerning_pair *ker = bsearch(&key, font->kernings, font->num_kernings, sizeof(kerning_pair), DSF_kerning_pair_cmp); if (ker != NULL) { x1 += ker->amount; x2 += ker->amount; font->pointer_x += ker->amount; } font->last_codepoint = codepoint; return DSF_NO_ERROR; } static dsf_error DSF_CodepointRender3D(dsf_handle handle, uint32_t codepoint, int16_t z) { if ((handle == 0) || (codepoint == 0)) return DSF_INVALID_ARGUMENT; dsf_font_internal_state *font = (dsf_font_internal_state *)handle; if (codepoint == '\n') { font->pointer_x = font->box_left; font->pointer_y += font->line_height; return DSF_NO_ERROR; } const block_char *ch = DSF_CodepointFindGlyph(handle, codepoint); if (ch == NULL) return DSF_CODEPOINT_NOT_FOUND; int tx1 = ch->x; int tx2 = tx1 + ch->width; int ty1 = ch->y; int ty2 = ty1 + ch->height; int x1 = font->pointer_x; int x2 = x1 + ch->width; int y1 = font->pointer_y; int y2 = y1 + ch->height; x1 += ch->xoffset; x2 += ch->xoffset; y1 += ch->yoffset; y2 += ch->yoffset; font->pointer_x += ch->xadvance; // Kerning pair to find kerning_pair key = { 0 }; key.first = font->last_codepoint; key.second = codepoint; const kerning_pair *ker = bsearch(&key, font->kernings, font->num_kernings, sizeof(kerning_pair), DSF_kerning_pair_cmp); if (ker != NULL) { x1 += ker->amount; x2 += ker->amount; font->pointer_x += ker->amount; } font->last_codepoint = codepoint; GFX_TEX_COORD = TEXTURE_PACK(inttot16(tx1), inttot16(ty1)); GFX_VERTEX16 = (y1 << 16) | (x1 & 0xFFFF); // Up-left GFX_VERTEX16 = z; GFX_TEX_COORD = TEXTURE_PACK(inttot16(tx1), inttot16(ty2)); GFX_VERTEX_XY = (y2 << 16) | (x1 & 0xFFFF); // Down-left GFX_TEX_COORD = TEXTURE_PACK(inttot16(tx2), inttot16(ty2)); GFX_VERTEX_XY = (y2 << 16) | (x2 & 0xFFFF); // Down-right GFX_TEX_COORD = TEXTURE_PACK(inttot16(tx2), inttot16(ty1)); GFX_VERTEX_XY = (y1 << 16) | (x2 & 0xFFFF); // Up-right return DSF_NO_ERROR; } dsf_error DSF_StringRenderDryRunWithCursor(dsf_handle handle, const char *str, size_t *size_x, size_t *size_y, size_t *final_x, size_t *final_y) { if ((handle == 0) || (str == NULL) || (size_x == NULL) || (size_y == NULL) || (final_x == NULL) || (final_y == NULL)) return DSF_INVALID_ARGUMENT; dsf_font_internal_state *font = (dsf_font_internal_state *)handle; if (strlen(str) == 0) { *size_x = 0; *size_y = 0; *final_x = font->pointer_x; *final_y = font->pointer_y; return DSF_NO_ERROR; } dsf_error ret = DSF_NO_ERROR; font->pointer_x = 0; font->pointer_y = 0; font->box_left = 0; font->box_top = 0; font->last_codepoint = 0; const char *readptr = str; size_t max_x = 0; size_t max_y = 0; while (*readptr != '\0') { uint32_t codepoint; size_t size = DSF_UTF8_CodepointRead(readptr, &codepoint); readptr += size; ret = DSF_CodepointRenderDryRun(handle, codepoint); if (ret != DSF_NO_ERROR) break; if (font->pointer_x > max_x) max_x = font->pointer_x; if (font->pointer_y > max_y) max_y = font->pointer_y; } *size_x = max_x; *size_y = max_y + font->line_height; return ret; } dsf_error DSF_StringRenderDryRun(dsf_handle handle, const char *str, size_t *size_x, size_t *size_y) { size_t final_x, final_y; return DSF_StringRenderDryRunWithCursor(handle, str, size_x, size_y, &final_x, &final_y); } dsf_error DSF_StringRender3DWithIndent(dsf_handle handle, const char *str, int32_t x, int32_t y, int32_t z, int32_t xStart) { if ((handle == 0) || (str == NULL)) return DSF_INVALID_ARGUMENT; dsf_error ret = DSF_NO_ERROR; dsf_font_internal_state *font = (dsf_font_internal_state *)handle; font->pointer_x = x + xStart; font->pointer_y = y; font->box_left = x; font->box_top = y; font->last_codepoint = 0; const char *readptr = str; glBegin(GL_QUADS); while (*readptr != '\0') { uint32_t codepoint; size_t size = DSF_UTF8_CodepointRead(readptr, &codepoint); readptr += size; ret = DSF_CodepointRender3D(handle, codepoint, z); if (ret != DSF_NO_ERROR) break; } glEnd(); return ret; } dsf_error DSF_StringRender3D(dsf_handle handle, const char *str, int32_t x, int32_t y, int32_t z) { return DSF_StringRender3DWithIndent(handle, str, x, y, z, 0); } dsf_error DSF_StringRender3DAlphaWithIndent(dsf_handle handle, const char *str, int32_t x, int32_t y, int32_t z, uint32_t poly_fmt, int poly_id_base, int32_t xStart) { if ((handle == 0) || (str == NULL)) return DSF_INVALID_ARGUMENT; dsf_error ret = DSF_NO_ERROR; dsf_font_internal_state *font = (dsf_font_internal_state *)handle; font->pointer_x = x + xStart; font->pointer_y = y; font->box_left = x; font->box_top = y; font->last_codepoint = 0; const char *readptr = str; int id_index = 0; while (*readptr != '\0') { uint32_t codepoint; size_t size = DSF_UTF8_CodepointRead(readptr, &codepoint); readptr += size; glPolyFmt(poly_fmt | POLY_ID(poly_id_base + id_index)); id_index ^= 1; glBegin(GL_QUADS); ret = DSF_CodepointRender3D(handle, codepoint, z); glEnd(); if (ret != DSF_NO_ERROR) break; } return ret; } dsf_error DSF_StringRender3DAlpha(dsf_handle handle, const char *str, int32_t x, int32_t y, int32_t z, uint32_t poly_fmt, int poly_id_base) { return DSF_StringRender3DAlphaWithIndent(handle, str, x, y, z, poly_fmt, poly_id_base, 0); } static dsf_error DSF_CodepointRenderBuffer(dsf_handle handle, uint32_t codepoint, unsigned int texture_fmt, const void *font_texture, size_t font_width, size_t font_height, void *out_texture, size_t out_width, size_t out_height) { // Don't check if the arguments are valid, this is an internal function dsf_font_internal_state *font = (dsf_font_internal_state *)handle; if (codepoint == '\n') { font->pointer_x = font->box_left; font->pointer_y += font->line_height; return DSF_NO_ERROR; } const block_char *ch = DSF_CodepointFindGlyph(handle, codepoint); if (ch == NULL) return DSF_CODEPOINT_NOT_FOUND; int tx1 = ch->x; int ty1 = ch->y; int x1 = font->pointer_x; int x2 = x1 + ch->width; int y1 = font->pointer_y; int y2 = y1 + ch->height; x1 += ch->xoffset; x2 += ch->xoffset; y1 += ch->yoffset; y2 += ch->yoffset; font->pointer_x += ch->xadvance; // Kerning pair to find kerning_pair key = { 0 }; key.first = font->last_codepoint; key.second = codepoint; const kerning_pair *ker = bsearch(&key, font->kernings, font->num_kernings, sizeof(kerning_pair), DSF_kerning_pair_cmp); if (ker != NULL) { x1 += ker->amount; x2 += ker->amount; font->pointer_x += ker->amount; } font->last_codepoint = codepoint; if (texture_fmt == GL_RGB256) { const uint8_t *src = font_texture; uint8_t *dst = out_texture; src += tx1 + ty1 * font_width; dst += x1 + y1 * out_width; for (int y = 0; y <= y2 - y1; y++) { const uint8_t *src_row = src; uint8_t *dst_row = dst; for (int x = 0; x <= x2 - x1; x++) { uint8_t color = *src_row++; if (color != 0) *dst_row = color; dst_row++; } src += font_width; dst += out_width; } } else if (texture_fmt == GL_RGBA) { const uint16_t *src = font_texture; uint16_t *dst = out_texture; src += tx1 + ty1 * font_width; dst += x1 + y1 * out_width; for (int y = 0; y <= y2 - y1; y++) { const uint16_t *src_row = src; uint16_t *dst_row = dst; for (int x = 0; x <= x2 - x1; x++) { uint16_t color = *src_row++; if (color & BIT(15)) *dst_row = color; dst_row++; } src += font_width; dst += out_width; } } else if ((texture_fmt == GL_RGB32_A3) || (texture_fmt == GL_RGB8_A5)) { const uint8_t *src = font_texture; uint8_t *dst = out_texture; src += tx1 + ty1 * font_width; dst += x1 + y1 * out_width; int alpha_shift; if (texture_fmt == GL_RGB32_A3) alpha_shift = 5; else // if (texture_fmt == GL_RGB8_A5) alpha_shift = 3; for (int y = 0; y <= y2 - y1; y++) { const uint8_t *src_row = src; uint8_t *dst_row = dst; for (int x = 0; x <= x2 - x1; x++) { uint8_t color = *src_row++; // We can't really blend two different colors because we're // limited by the palette. For that reason, if the new alpha is // 0 we leave the old entry. If the new alpha is anything other // than 0, we replace the old entry by the new entry, even if // the result is incorrect. if ((color >> alpha_shift) != 0) *dst_row = color; dst_row++; } src += font_width; dst += out_width; } } else if (texture_fmt == GL_RGB16) { const uint8_t *src = font_texture; uint8_t *dst = out_texture; for (int y = 0; y <= y2 - y1; y++) { const uint8_t *src_row = src + (((ty1 + y) * font_width) >> 1); uint8_t *dst_row = dst + (((y1 + y) * out_width) >> 1); for (int x = 0; x <= x2 - x1; x++) { const uint8_t *src_px = src_row + ((tx1 + x) >> 1); uint8_t *dst_px = dst_row + ((x1 + x) >> 1); int shift = ((tx1 + x) & 1) * 4; uint8_t src_color = (*src_px >> shift) & 0xF; if (src_color == 0) continue; shift = ((x1 + x) & 1) * 4; uint8_t mask = ~(0xF << shift); *dst_px = (*dst_px & mask) | (src_color << shift); } } } else if (texture_fmt == GL_RGB4) { const uint8_t *src = font_texture; uint8_t *dst = out_texture; for (int y = 0; y <= y2 - y1; y++) { const uint8_t *src_row = src + (((ty1 + y) * font_width) >> 2); uint8_t *dst_row = dst + (((y1 + y) * out_width) >> 2); for (int x = 0; x <= x2 - x1; x++) { const uint8_t *src_px = src_row + ((tx1 + x) >> 2); uint8_t *dst_px = dst_row + ((x1 + x) >> 2); int shift = ((tx1 + x) & 3) * 2; uint8_t src_color = (*src_px >> shift) & 0x3; if (src_color == 0) continue; shift = ((x1 + x) & 3) * 2; uint8_t mask = ~(0x3 << shift); *dst_px = (*dst_px & mask) | (src_color << shift); } } } return DSF_NO_ERROR; } dsf_error DSF_StringRenderToTexture(dsf_handle handle, const char *str, unsigned int texture_fmt, const void *font_texture, size_t font_width, size_t font_height, void **out_texture, size_t *out_width, size_t *out_height) { if ((handle == 0) || (str == NULL) || (font_texture == NULL) || (out_texture == NULL) || (out_width == NULL) || (out_height == NULL)) return DSF_INVALID_ARGUMENT; if ((texture_fmt < 1) || (texture_fmt > 7) || (texture_fmt == GL_COMPRESSED)) return DSF_TEXTURE_BAD_FORMAT; if ((font_width == 0) || (font_height == 0)) return DSF_INVALID_ARGUMENT; if (strlen(str) == 0) return DSF_INVALID_ARGUMENT; // Start process dsf_error ret = DSF_NO_ERROR; dsf_font_internal_state *font = (dsf_font_internal_state *)handle; // Get size size_t tex_width, tex_height; ret = DSF_StringRenderDryRun(handle, str, &tex_width, &tex_height); if (ret != DSF_NO_ERROR) return ret; if ((tex_width > 1024) || (tex_height > 1024)) return DSF_TEXTURE_TOO_BIG; // Expand to a valid texture size for (size_t i = 8; i <= 1024; i <<= 1) { if (tex_width <= i) { tex_width = i; break; } } for (size_t i = 8; i <= 1024; i <<= 1) { if (tex_height <= i) { tex_height = i; break; } } const int size_shift[] = { 0, // Nothing 1, // A3PAL32 3, // PAL4 2, // PAL16 1, // PAL256 0, // TEX4X4 (This value isn't used) 1, // A5PAL8 0, // A1RGB5 0, // RGB5 }; size_t tex_size = (2 * tex_width * tex_height) >> size_shift[texture_fmt]; // Allocate buffer void *tex_buffer = calloc(1, tex_size); if (tex_buffer == NULL) return DSF_NO_MEMORY; // Render string font->pointer_x = 0; font->pointer_y = 0; font->box_left = 0; font->box_top = 0; font->last_codepoint = 0; const char *readptr = str; while (*readptr != '\0') { uint32_t codepoint; size_t size = DSF_UTF8_CodepointRead(readptr, &codepoint); readptr += size; ret = DSF_CodepointRenderBuffer(handle, codepoint, texture_fmt, font_texture, font_width, font_height, tex_buffer, tex_width, tex_height); if (ret != DSF_NO_ERROR) break; } // Return texture information *out_texture = tex_buffer; *out_width = tex_width; *out_height = tex_height; return ret; }