library: Add rich text support

This support relies on BMFont: https://www.angelcode.com/products/bmfont/

Nitro Engine doesn't support that format directly. Instead, it uses
LibDSF internally: https://github.com/AntonioND/libdsf
This commit is contained in:
Antonio Niño Díaz 2024-03-02 12:44:11 +00:00
parent 951f0f87ca
commit 50e14b2c21
8 changed files with 1678 additions and 4 deletions

View File

@ -15,9 +15,9 @@ include $(DEVKITARM)/ds_rules
# DATA is a list of directories containing data files
# INCLUDES is a list of directories containing header files
#---------------------------------------------------------------------------------
SOURCES := source source/dsma
SOURCES := source source/dsma source/libdsf
DATA := data
INCLUDES := include
INCLUDES := include source/libdsf
ifeq ($(NE_DEBUG),1)
TARGET := NE_debug

View File

@ -12,7 +12,7 @@ ARM_NONE_EABI_PATH ?= $(WONDERFUL_TOOLCHAIN)/toolchain/gcc-arm-none-eabi/bin/
# -----------------
SOURCEDIRS := source
INCLUDEDIRS := include
INCLUDEDIRS := include source/libdsf
GFXDIRS :=
BINDIRS :=

View File

@ -37,6 +37,7 @@ extern "C" {
#include "NEPalette.h"
#include "NEPhysics.h"
#include "NEPolygon.h"
#include "NERichText.h"
#include "NEText.h"
#include "NETexture.h"

168
include/NERichText.h Normal file
View File

@ -0,0 +1,168 @@
// SPDX-License-Identifier: MIT
//
// Copyright (c) 2024 Antonio Niño Díaz
//
// This file is part of Nitro Engine
#ifndef NE_RICHTEXT_H__
#define NE_RICHTEXT_H__
/// @file NERichText.h
/// @brief Rich text system.
/// @defgroup text_system Text system
///
/// Rich text drawing functions. It is possible to draw text as a series of
/// quad, or to render text to a material (which needs to be drawn by the
/// user as a single quad).
///
/// The functions accept '\n', but not things like '%d', '%s', etc. You'll need
/// to use snprintf() or similar for that.
///
/// You need to call NE_2DViewInit() before using any of the quad drawing
/// functions.
///
/// In order to generate fonts, please check the website of BMFont:
///
/// https://www.angelcode.com/products/bmfont/
///
/// You need to export fonts in binary format, and the images in PNG format.
///
/// @{
#define NE_MAX_RICH_TEXT_FONTS 8 ///< Default max number of rich text fonts
/// Change the priority of rich text drawn after this function call.
///
/// @param priority New priority.
void NE_RichTextPrioritySet(int priority);
/// Set to 0 the priority of rich text drawn after this function call.
void NE_RichTextPriorityReset(void);
/// Initialize a rich text slot.
///
/// @param slot The slot to initialize (from 0 to NE_MAX_RICH_TEXT_FONTS).
void NE_RichTextInit(u32 slot);
/// End a rich text slot and free all the resources used by it.
///
/// @param slot The slot to end.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextEnd(u32 slot);
/// Load font metadata from the specified file in the filesystem.
///
/// @param slot The slot to use.
/// @param path Path to a '.fnt' binary file.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextMetadataLoadFAT(u32 slot, const char *path);
/// Load font metadata from the buffer in RAM.
///
/// @param slot The slot to use.
/// @param data Pointer to a '.fnt' binary file in RAM
/// @param data_size Size of the file.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextMetadataLoadMemory(u32 slot, const void *data, size_t data_size);
/// Load a GRF file to be used as material when drawing text as 3D quads.
///
/// This loads a GRF file with a texture and loads it to VRAM as a material.
///
/// This is required for NE_RichTextRender3D() and NE_RichTextRender3DAlpha().
///
/// This isn't required for NE_RichTextRenderMaterial().
///
/// @param slot The slot to use.
/// @param path Path to the GRF file.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextMaterialLoadGRF(u32 slot, const char *path);
/// Assign a material to the slot, which will be used to draw text as 3D quads.
///
/// After this call, the font will own the material and texture, and it will
/// delete them when the font is deleted. The caller of this function must not
/// try to delete the material or palette manually after calling this function.
///
/// This is required for NE_RichTextRender3D() and NE_RichTextRender3DAlpha().
///
/// This isn't required for NE_RichTextRenderMaterial().
///
/// @param slot The slot to use.
/// @param mat The material to assign.
/// @param pal The palette to assign.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextMaterialSet(u32 slot, NE_Material *mat, NE_Palette *pal);
/// Load a GRF file to RAM to be used to render text to textures.
///
/// This doesn't load the texture to VRAM, it keeps it in RAM. The texture will
/// be used whenever the user uses NE_RichTextRenderMaterial() to render text to
/// a new material.
///
/// This is required for NE_RichTextRenderMaterial().
///
/// This isn't required for NE_RichTextRender3D() or NE_RichTextRender3DAlpha().
///
/// @param slot The slot to use.
/// @param path Path to the GRF file.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextBitmapLoadGRF(u32 slot, const char *path);
/// Render a string by rendering one 3D quad per codepoint.
///
/// This preserves the polygon format that is currently active.
///
/// @param slot The slot to use.
/// @param str The string to render.
/// @param x The left coordinate of the text.
/// @param y The top coordinate of the text.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextRender3D(u32 slot, const char *str, s32 x, s32 y);
/// Render a string by rendering one 3D quad per codepoint with alternating
/// polygon IDs.
///
/// This function will alternate between polygon IDs so that alpha blending
/// works between multiple polygons when they overlap. This is a requirement of
/// the NDS 3D hardware.
///
/// It is required to pass the base polygon format as a parameter because the
/// polygon format data is write-only. Whenever the polygon ID needs to be
/// changed, the rest of the polygon format flags need to be set as well.
///
/// @param slot The slot to use.
/// @param str The string to render.
/// @param x The left coordinate of the text.
/// @param y The top coordinate of the text.
/// @param poly_fmt The polygon format values to be used for the quads.
/// @param poly_id_base The base polygon ID to use for the quads.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextRender3DAlpha(u32 slot, const char *str, s32 x, s32 y,
uint32_t poly_fmt, int poly_id_base);
/// Render a string with the specified font and create a material from it.
///
/// This function renders the string to a buffer and creates a material that
/// uses that buffer as texture. This is quite CPU-intensive, but it makes sense
/// to do it if the text is going to be displayed unchanged for a long time.
///
/// If the font uses an image that doesn't use palette, it will return a
/// material and it won't try to return a palette. If the font uses an image
/// with a palette, there are two options. If you want the pointer to the
/// palette, the function will give it to you. If you don't want the pointer,
/// you can pass NULL to that argument, and the palette will be autodeleted when
/// the material is deleted.
///
/// @param slot The font slot to use.
/// @param str The string to print.
/// @param mat A pointer to a NE_Material to store the new material.
/// @param pal A pointer to a NE_Palette to store the new palette, or NULL.
/// @return Returns 1 on success, 0 on failure.
int NE_RichTextRenderMaterial(u32 slot, const char *str, NE_Material **mat,
NE_Palette **pal);
/// @}
#endif // NE_RICHTEXT_H__

View File

@ -17,7 +17,7 @@
/// for that.
///
/// You need to call NE_2DViewInit() before using any of the text drawing
/// functions..
/// functions.
///
/// @{

383
source/NERichText.c Normal file
View File

@ -0,0 +1,383 @@
// SPDX-License-Identifier: MIT
//
// Copyright (c) 2024 Antonio Niño Díaz
//
// This file is part of Nitro Engine
#include "NEMain.h"
#include "libdsf/dsf.h"
/// @file NERichText.c
typedef struct {
// Fields used when the font texture is stored in VRAM
NE_Material *material;
NE_Palette *palette;
// Fields used when the font texture is stored in RAM
NE_TextureFormat fmt;
void *texture_buffer;
size_t texture_width;
size_t texture_height;
void *palette_buffer;
size_t palette_size;
// Other fields
dsf_handle handle;
bool active;
} ne_rich_textinfo_t;
static ne_rich_textinfo_t NE_RichTextInfo[NE_MAX_RICH_TEXT_FONTS];
static int NE_RICH_TEXT_PRIORITY = 0;
void NE_RichTextInit(u32 slot)
{
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (info->active)
NE_RichTextEnd(slot);
memset(info, 0, sizeof(ne_rich_textinfo_t));
info->active = true;
}
int NE_RichTextEnd(u32 slot)
{
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
if (info->material != NULL)
NE_MaterialDelete(info->material);
if (info->palette != NULL)
NE_PaletteDelete(info->palette);
if (info->texture_buffer != NULL)
free(info->texture_buffer);
if (info->palette_buffer != NULL)
free(info->palette_buffer);
int ret = 0;
if (info->handle)
{
dsf_error err = DSF_FreeFont(&(info->handle));
if (err != DSF_NO_ERROR)
ret = err;
}
memset(info, 0, sizeof(ne_rich_textinfo_t));
if (ret != 0)
return 0;
return 1;
}
int NE_RichTextMetadataLoadFAT(u32 slot, const char *path)
{
NE_AssertPointer(path, "NULL path pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
dsf_handle handle;
dsf_error ret = DSF_LoadFontFilesystem(&handle, path);
if (ret != DSF_NO_ERROR)
{
NE_DebugPrint("DSF_LoadFontFilesystem(): %d\n", ret);
return 0;
}
info->handle = handle;
return 0;
}
int NE_RichTextMetadataLoadMemory(u32 slot, const void *data, size_t data_size)
{
NE_AssertPointer(data, "NULL data pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
dsf_handle handle;
dsf_error ret = DSF_LoadFontMemory(&handle, data, data_size);
if (ret != DSF_NO_ERROR)
{
NE_DebugPrint("DSF_LoadFontMemory(): %d\n", ret);
return 0;
}
info->handle = handle;
return 1;
}
int NE_RichTextMaterialLoadGRF(u32 slot, const char *path)
{
NE_AssertPointer(path, "NULL path pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
info->material = NE_MaterialCreate();
info->palette = NE_PaletteCreate();
int ret = NE_MaterialTexLoadGRF(info->material, info->palette,
NE_TEXGEN_TEXCOORD | NE_TEXTURE_COLOR0_TRANSPARENT, path);
if (ret == 0)
{
NE_MaterialDelete(info->material);
NE_PaletteDelete(info->palette);
return 0;
}
return 1;
}
int NE_RichTextMaterialSet(u32 slot, NE_Material *mat, NE_Palette *pal)
{
NE_AssertPointer(mat, "NULL material pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
info->material = mat;
info->palette = pal;
return 1;
}
int NE_RichTextBitmapLoadGRF(u32 slot, const char *path)
{
#ifndef NE_BLOCKSDS
(void)tex;
(void)pal;
(void)flags;
(void)path;
NE_DebugPrint("%s only supported in BlocksDS", __func__);
return 0;
#else // NE_BLOCKSDS
NE_AssertPointer(path, "NULL path pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
if (info->texture_buffer != NULL)
free(info->texture_buffer);
if (info->palette_buffer != NULL)
free(info->palette_buffer);
int ret = 0;
void *gfxDst = NULL;
void *palDst = NULL;
size_t palSize;
GRFHeader header = { 0 };
GRFError err = grfLoadPath(path, &header, &gfxDst, NULL, NULL, NULL,
&palDst, &palSize);
if (err != GRF_NO_ERROR)
{
NE_DebugPrint("Couldn't load GRF file: %d", err);
goto cleanup;
}
if (gfxDst == NULL)
{
NE_DebugPrint("No graphics found in GRF file");
goto cleanup;
}
bool palette_required = true;
switch (header.gfxAttr)
{
case GRF_TEXFMT_A5I3:
info->fmt = NE_A5PAL8;
break;
case GRF_TEXFMT_A3I5:
info->fmt = NE_A3PAL32;
break;
case GRF_TEXFMT_4x4:
info->fmt = NE_TEX4X4;
break;
case 16:
info->fmt = NE_A1RGB5;
palette_required = false;
break;
case 8:
info->fmt = NE_PAL256;
break;
case 4:
info->fmt = NE_PAL16;
break;
case 2:
info->fmt = NE_PAL4;
break;
default:
NE_DebugPrint("Invalid format in GRF file");
goto cleanup;
}
info->texture_buffer = gfxDst;
info->texture_width = header.gfxWidth;
info->texture_height = header.gfxHeight;
if (palDst != NULL)
{
info->palette_buffer = palDst;
info->palette_size = palSize;
}
else
{
if (palette_required)
{
NE_DebugPrint("No palette found in GRF, but format requires it");
goto cleanup;
}
info->palette_buffer = NULL;
info->palette_size = 0;
}
ret = 1; // Success
cleanup:
free(gfxDst);
free(palDst);
return ret;
#endif // NE_BLOCKSDS
}
int NE_RichTextRender3D(u32 slot, const char *str, s32 x, s32 y)
{
NE_AssertPointer(str, "NULL str pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
NE_MaterialUse(info->material);
dsf_error err = DSF_StringRender3D(info->handle, str, x, y,
NE_RICH_TEXT_PRIORITY);
if (err != DSF_NO_ERROR)
return 0;
return 1;
}
int NE_RichTextRender3DAlpha(u32 slot, const char *str, s32 x, s32 y,
uint32_t poly_fmt, int poly_id_base)
{
NE_AssertPointer(str, "NULL str pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
NE_MaterialUse(info->material);
dsf_error err = DSF_StringRender3DAlpha(info->handle, str, x, y,
NE_RICH_TEXT_PRIORITY,
poly_fmt, poly_id_base);
if (err != DSF_NO_ERROR)
return 0;
return 1;
}
int NE_RichTextRenderMaterial(u32 slot, const char *str, NE_Material **mat,
NE_Palette **pal)
{
NE_AssertPointer(str, "NULL str pointer");
NE_AssertPointer(mat, "NULL mat pointer");
NE_AssertPointer(pal, "NULL pal pointer");
if (slot >= NE_MAX_RICH_TEXT_FONTS)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
void *out_texture;
size_t out_width, out_height;
dsf_error err = DSF_StringRenderToTexture(info->handle,
str, info->fmt, info->texture_buffer,
info->texture_width, info->texture_height,
&out_texture, &out_width, &out_height);
if (err != DSF_NO_ERROR)
{
free(out_texture);
return 0;
}
*mat = NE_MaterialCreate();
if (NE_MaterialTexLoad(*mat, info->fmt, out_width, out_height,
NE_TEXGEN_TEXCOORD | NE_TEXTURE_COLOR0_TRANSPARENT,
out_texture) == 0)
{
free(out_texture);
return 0;
}
if (info->palette_buffer != NULL)
{
NE_Palette *palette = NE_PaletteCreate();
if (NE_PaletteLoad(palette, info->palette_buffer,
info->palette_size / 2, info->fmt) == 0)
{
NE_MaterialDelete(*mat);
free(out_texture);
return 0;
}
NE_MaterialSetPalette(*mat, palette);
// If the caller has requested the pointer to the palette, return the
// value to the user. If not, mark it to be autodeleted.
if (pal)
*pal = palette;
else
NE_MaterialAutodeletePalette(*mat);
}
// This isn't needed after it has been loaded to VRAM
free(out_texture);
return 1;
}

949
source/libdsf/dsf.c Normal file
View File

@ -0,0 +1,949 @@
// SPDX-License-Identifier: Zlib
//
// Copyright (c) 2024 Antonio Niño Díaz
#include <stdint.h>
#include <stdio.h>
#include <nds.h>
#include <dsf.h>
// 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 index will be
// saved here for ease of access.
int32_t replacement_character_id;
} dsf_font_internal_state;
// Functions
// ---------
static int DSF_CodepointFindGlyphIndex(dsf_handle handle, uint32_t codepoint)
{
dsf_font_internal_state *font = (dsf_font_internal_state *)handle;
for (size_t i = 0; i < font->num_chars; i++)
{
block_char *ch = &(font->chars[i]);
if (ch->id == codepoint)
return i;
}
return font->replacement_character_id;
}
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);
}
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);
}
}
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 -1 so that DSF_CodepointFindGlyphIndex() returns
// -1 if no replacement character glyph is found.
font->replacement_character_id = -1;
font->replacement_character_id =
DSF_CodepointFindGlyphIndex(*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;
}
int index = DSF_CodepointFindGlyphIndex(handle, codepoint);
if (index < 0)
return DSF_CODEPOINT_NOT_FOUND;
block_char *ch = &(font->chars[index]);
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;
for (size_t i = 0; i < font->num_kernings; i++)
{
kerning_pair *ker = &(font->kernings[i]);
if (ker->first != font->last_codepoint)
continue;
if (ker->second != codepoint)
continue;
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;
}
int index = DSF_CodepointFindGlyphIndex(handle, codepoint);
if (index < 0)
return DSF_CODEPOINT_NOT_FOUND;
block_char *ch = &(font->chars[index]);
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;
for (size_t i = 0; i < font->num_kernings; i++)
{
kerning_pair *ker = &(font->kernings[i]);
if (ker->first != font->last_codepoint)
continue;
if (ker->second != codepoint)
continue;
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_StringRenderDryRun(dsf_handle handle, const char *str,
size_t *size_x, size_t *size_y)
{
if ((handle == 0) || (str == NULL) || (size_x == NULL) || (size_y == NULL))
return DSF_INVALID_ARGUMENT;
if (strlen(str) == 0)
{
*size_x = 0;
*size_y = 0;
return DSF_NO_ERROR;
}
dsf_error ret = DSF_NO_ERROR;
dsf_font_internal_state *font = (dsf_font_internal_state *)handle;
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_StringRender3D(dsf_handle handle, const char *str,
int32_t x, int32_t y, int32_t z)
{
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;
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_StringRender3DAlpha(dsf_handle handle, const char *str,
int32_t x, int32_t y, int32_t z,
uint32_t poly_fmt, int poly_id_base)
{
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;
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;
}
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;
}
int index = DSF_CodepointFindGlyphIndex(handle, codepoint);
if (index < 0)
return DSF_CODEPOINT_NOT_FOUND;
block_char *ch = &(font->chars[index]);
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;
for (size_t i = 0; i < font->num_kernings; i++)
{
kerning_pair *ker = &(font->kernings[i]);
if (ker->first != font->last_codepoint)
continue;
if (ker->second != codepoint)
continue;
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);
memset(tex_buffer, 0, 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;
}

173
source/libdsf/dsf.h Normal file
View File

@ -0,0 +1,173 @@
// SPDX-License-Identifier: Zlib
//
// Copyright (c) 2024 Antonio Niño Díaz
#ifndef DSF_H__
#define DSF_H__
/// @file dsf.h
///
/// @brief Global include of LibDSF.
#include <stddef.h>
#include <stdint.h>
/// @defgroup libdsf_types LibDSF definitions and types.
/// @{
/// Major version of LibDSF (semver)
#define LIBDSF_VERSION_MAJOR 0
/// Minor version of LibDSF (semver)
#define LIBDSF_VERSION_MINOR 1
/// Patch version of LibDSF (semver)
#define LIBDSF_VERSION_PATCH 0
/// Value that combines all version numbers, useful for version checks.
#define LIBDSF_VERSION \
((LIBDSF_VERSION_MAJOR << 16) | \
(LIBDSF_VERSION_MINOR << 8) | \
(LIBDSF_VERSION_PATCH << 0))
/// Version string
#define LIBDSF_VERSION_STRING "0.1.0"
/// Character that is used when there are UTF-8 decoding errors.
///
/// This is returned when a character with an invalid encoding is found.
#define REPLACEMENT_CHARACTER 0xFFFD
/// List of possible errors of the library.
typedef enum {
DSF_NO_ERROR = 0, ///< No error
DSF_INVALID_ARGUMENT = -1, ///< Invalid argument (e.g. NULL pointer)
DSF_BAD_MAGIC = -2, ///< Bad BMFont magic number
DSF_BAD_VERSION = -3, ///< Bad BMFont version
DSF_BAD_CHUNK_SIZE = -4, ///< The size of a BMFont chunk is wrong
DSF_NO_MEMORY = -5, ///< Not enough RAM to allocate font data
DSF_UNEXPECTED_END = -6, ///< File ends unexpectedly
DSF_NO_CHARACTERS = -7, ///< No characters present in font
DSF_FILE_OPEN_ERROR = -8, ///< Can't open the provided file
DSF_FILE_EMPTY = -9, ///< The provided file is empty
DSF_FILE_SEEK_ERROR = -10, ///< Can't fseek() file
DSF_FILE_READ_ERROR = -11, ///< Can't read file
DSF_CODEPOINT_NOT_FOUND = -12, ///< Codepoint not present in font
DSF_TEXTURE_TOO_BIG = -12, ///< The texture size required is too big for the NDS
DSF_TEXTURE_BAD_FORMAT = -13, ///< Unsupported NDS texture format
} dsf_error;
/// Type that represents a DSF font internal state.
typedef uintptr_t dsf_handle;
/// @}
/// @defgroup libdsf_load_unload Font loading and unloading functions.
/// @{
/// This function loads a BMFont in binary format from RAM.
///
/// @param handle Pointer to a handle that will be configured for this font.
/// @param data Pointer to the font data (".fnt" file).
/// @param data_size Size of the font data.
///
/// @return An error code or DSF_NO_ERROR on success.
dsf_error DSF_LoadFontMemory(dsf_handle *handle,
const void *data, int32_t data_size);
/// This function loads a BMFont in binary format from the filesystem.
///
/// @param handle Pointer to a handle that will be configured for this font.
/// @param path Path to the ".fnt" file in the filesystem.
///
/// @return An error code or DSF_NO_ERROR on success.
dsf_error DSF_LoadFontFilesystem(dsf_handle *handle, const char *path);
/// Free all memory used by this font.
///
/// It also invalidates the provided handler.
///
/// @param handle Pointer to the font handler.
///
/// @return An error code or DSF_NO_ERROR on success.
dsf_error DSF_FreeFont(dsf_handle *handle);
/// @}
/// @defgroup libdsf_render Functions to draw text strings.
/// @{
/// Pretend to render a string to calculate its final size once rendered.
///
/// @param handle Handler of the font to use.
/// @param str String to print.
/// @param size_x Pointer to a variable to store the size.
/// @param size_y Pointer to a variable to store the size.
///
/// @return An error code or DSF_NO_ERROR on success.
dsf_error DSF_StringRenderDryRun(dsf_handle handle, const char *str,
size_t *size_x, size_t *size_y);
/// Render a string by rendering one 3D quad per codepoint.
///
/// @param handle Handler of the font to use.
/// @param str String to print.
/// @param x Top x coordinate (0 to 255, but you can go outside of that).
/// @param y Left y coordinate (0 to 191, but you can go outside of that).
/// @param z Z coordinate (depth).
///
/// @return An error code or DSF_NO_ERROR on success.
dsf_error DSF_StringRender3D(dsf_handle handle, const char *str,
int32_t x, int32_t y, int32_t z);
/// Render a string by rendering one 3D quad per codepoint with alternating
/// polygon IDs.
///
/// This function will alternate between polygon IDs so that alpha blending
/// works between multiple polygons when they overlap. This is a requirement of
/// the NDS 3D hardware.
///
/// It is required to pass the base polygon format as a parameter because the
/// polygon format data is write-only. Whenever the polygon ID needs to be
/// changed, the rest of the polygon format flags need to be set as well.
///
/// @param handle Handler of the font to use.
/// @param str String to print.
/// @param x Top x coordinate (0 to 255, but you can go outside of that).
/// @param y Left y coordinate (0 to 191, but you can go outside of that).
/// @param z Z coordinate (depth).
/// @param poly_fmt Polygon formats to apply to the characters.
/// @param poly_id_base poly_id_base and poly_id_base + 1 will be used.
///
/// @return An error code or DSF_NO_ERROR on success.
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);
/// Allocates a buffer and renders the provided string to that buffer.
///
/// This function takes a texture stored in RAM with the desired font. It will
/// calculate the final size of the text it has to print, and it will allocate
/// enough space for it. Note that NDS texture sizes must be powers of two, so
/// the end result may be bigger than the actual text. After allocating the
/// buffer, it will render the string to that buffer.
///
/// This function won't load the texture to VRAM. The returned buffer needs to
/// be loaded to texture VRAM to be used (with functions like glTexImage2D() or
/// NE_MaterialTexLoad()).
///
/// @param handle Handler of the font to use.
/// @param str String to print.
/// @param texture_fmt Texture format (GL_TEXTURE_TYPE_ENUM, NE_TextureFormat).
/// @param font_texture Pointer to the font texture data that contains the.
/// @param font_width Width of the font texture.
/// @param font_height Height of the font texture.
/// @param out_texture The pointer to the new buffer is returned here.
/// @param out_width The width to the new buffer is returned here.
/// @param out_height The height to the new buffer is returned here.
///
/// @return An error code or DSF_NO_ERROR on success.
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);
/// @}
#endif // DSF_H__