diff --git a/font/FUSION_PIXEL_FONT_OFL.txt b/font/FUSION_PIXEL_FONT_OFL.txt new file mode 100644 index 0000000..ed21510 --- /dev/null +++ b/font/FUSION_PIXEL_FONT_OFL.txt @@ -0,0 +1,96 @@ +Fusion Pixel Font +https://github.com/TakWolf/fusion-pixel-font + +Copyright (c) 2022, TakWolf (https://takwolf.com). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/font/gbalatro_sys8.png b/font/gbalatro_sys8.png index 607d62f..d068d6c 100644 Binary files a/font/gbalatro_sys8.png and b/font/gbalatro_sys8.png differ diff --git a/include/chinese_text.h b/include/chinese_text.h new file mode 100644 index 0000000..3666770 --- /dev/null +++ b/include/chinese_text.h @@ -0,0 +1,21 @@ +/** + * @file chinese_text.h + * @brief UTF-8 conversion helpers for the extended TTE font. + */ +#ifndef CHINESE_TEXT_H +#define CHINESE_TEXT_H + +#include + +/** + * @brief Map supported UTF-8 Chinese text to valid UTF-8 sequences for custom TTE glyphs. + * + * ASCII bytes, including TTE formatting tags, are copied unchanged. Unsupported Unicode + * characters are replaced with '?'. Custom glyph IDs are emitted as UTF-8 because Tonc TTE + * decodes every byte above 0x7F as part of a UTF-8 sequence. + * + * @return Number of encoded bytes, excluding the null terminator. + */ +size_t chinese_text_encode(const char* utf8, char* output, size_t output_size); + +#endif // CHINESE_TEXT_H diff --git a/include/def_chinese_glyph_table.h b/include/def_chinese_glyph_table.h new file mode 100644 index 0000000..9f4a8ff --- /dev/null +++ b/include/def_chinese_glyph_table.h @@ -0,0 +1,124 @@ +// Ordered glyphs appended after ASCII code point 127 in gbalatro_sys8.png. +CHINESE_GLYPH(0x5C0F) // 小 +CHINESE_GLYPH(0x4E11) // 丑 +CHINESE_GLYPH(0x724C) // 牌 +CHINESE_GLYPH(0x6C49) // 汉 +CHINESE_GLYPH(0x5316) // 化 +CHINESE_GLYPH(0x9884) // 预 +CHINESE_GLYPH(0x89C8) // 览 +CHINESE_GLYPH(0x534A) // 半 +CHINESE_GLYPH(0x5F20) // 张 +CHINESE_GLYPH(0x6050) // 恐 +CHINESE_GLYPH(0x6016) // 怖 +CHINESE_GLYPH(0x9762) // 面 +CHINESE_GLYPH(0x5B54) // 孔 +CHINESE_GLYPH(0x5982) // 如 +CHINESE_GLYPH(0x679C) // 果 +CHINESE_GLYPH(0x51FA) // 出 +CHINESE_GLYPH(0x4E0D) // 不 +CHINESE_GLYPH(0x8D85) // 超 +CHINESE_GLYPH(0x8FC7) // 过 +CHINESE_GLYPH(0x6253) // 打 +CHINESE_GLYPH(0x7684) // 的 +CHINESE_GLYPH(0x4EBA) // 人 +CHINESE_GLYPH(0x5934) // 头 +CHINESE_GLYPH(0x8BA1) // 计 +CHINESE_GLYPH(0x5206) // 分 +CHINESE_GLYPH(0x65F6) // 时 +CHINESE_GLYPH(0x7ED9) // 给 +CHINESE_GLYPH(0x4E88) // 予 +CHINESE_GLYPH(0x500D) // 倍 +CHINESE_GLYPH(0x7387) // 率 +CHINESE_GLYPH(0x7B79) // 筹 +CHINESE_GLYPH(0x7801) // 码 +CHINESE_GLYPH(0x5DE6) // 左 +CHINESE_GLYPH(0x53F3) // 右 +CHINESE_GLYPH(0x5207) // 切 +CHINESE_GLYPH(0x6362) // 换 +CHINESE_GLYPH(0x52A8) // 动 +CHINESE_GLYPH(0x753B) // 画 +CHINESE_GLYPH(0x4E3B) // 主 +CHINESE_GLYPH(0x83DC) // 菜 +CHINESE_GLYPH(0x5355) // 单 +CHINESE_GLYPH(0x4E00) // 一 +CHINESE_GLYPH(0x4E09) // 三 +CHINESE_GLYPH(0x4E0B) // 下 +CHINESE_GLYPH(0x4E24) // 两 +CHINESE_GLYPH(0x4E2A) // 个 +CHINESE_GLYPH(0x4E2D) // 中 +CHINESE_GLYPH(0x4E3A) // 为 +CHINESE_GLYPH(0x4E8E) // 于 +CHINESE_GLYPH(0x4F4D) // 位 +CHINESE_GLYPH(0x4F4E) // 低 +CHINESE_GLYPH(0x4F59) // 余 +CHINESE_GLYPH(0x4F8B) // 例 +CHINESE_GLYPH(0x4FA7) // 侧 +CHINESE_GLYPH(0x5076) // 偶 +CHINESE_GLYPH(0x518D) // 再 +CHINESE_GLYPH(0x51E0) // 几 +CHINESE_GLYPH(0x5230) // 到 +CHINESE_GLYPH(0x5236) // 制 +CHINESE_GLYPH(0x524D) // 前 +CHINESE_GLYPH(0x5269) // 剩 +CHINESE_GLYPH(0x529B) // 力 +CHINESE_GLYPH(0x52A0) // 加 +CHINESE_GLYPH(0x5305) // 包 +CHINESE_GLYPH(0x53D1) // 发 +CHINESE_GLYPH(0x53EF) // 可 +CHINESE_GLYPH(0x5408) // 合 +CHINESE_GLYPH(0x540C) // 同 +CHINESE_GLYPH(0x540E) // 后 +CHINESE_GLYPH(0x542B) // 含 +CHINESE_GLYPH(0x548C) // 和 +CHINESE_GLYPH(0x56DB) // 四 +CHINESE_GLYPH(0x56DE) // 回 +CHINESE_GLYPH(0x5747) // 均 +CHINESE_GLYPH(0x590D) // 复 +CHINESE_GLYPH(0x5916) // 外 +CHINESE_GLYPH(0x5947) // 奇 +CHINESE_GLYPH(0x5B50) // 子 +CHINESE_GLYPH(0x5BF9) // 对 +CHINESE_GLYPH(0x5C06) // 将 +CHINESE_GLYPH(0x5F03) // 弃 +CHINESE_GLYPH(0x5F53) // 当 +CHINESE_GLYPH(0x6210) // 成 +CHINESE_GLYPH(0x6216) // 或 +CHINESE_GLYPH(0x6240) // 所 +CHINESE_GLYPH(0x624B) // 手 +CHINESE_GLYPH(0x62E5) // 拥 +CHINESE_GLYPH(0x62EC) // 括 +CHINESE_GLYPH(0x63A5) // 接 +CHINESE_GLYPH(0x6570) // 数 +CHINESE_GLYPH(0x65B9) // 方 +CHINESE_GLYPH(0x65E0) // 无 +CHINESE_GLYPH(0x6700) // 最 +CHINESE_GLYPH(0x6709) // 有 +CHINESE_GLYPH(0x672C) // 本 +CHINESE_GLYPH(0x673A) // 机 +CHINESE_GLYPH(0x6761) // 条 +CHINESE_GLYPH(0x6765) // 来 +CHINESE_GLYPH(0x6843) // 桃 +CHINESE_GLYPH(0x6885) // 梅 +CHINESE_GLYPH(0x69FD) // 槽 +CHINESE_GLYPH(0x6B21) // 次 +CHINESE_GLYPH(0x6BCF) // 每 +CHINESE_GLYPH(0x70B9) // 点 +CHINESE_GLYPH(0x7247) // 片 +CHINESE_GLYPH(0x7528) // 用 +CHINESE_GLYPH(0x7531) // 由 +CHINESE_GLYPH(0x7A7A) // 空 +CHINESE_GLYPH(0x7B2C) // 第 +CHINESE_GLYPH(0x7EA2) // 红 +CHINESE_GLYPH(0x7EC4) // 组 +CHINESE_GLYPH(0x80FD) // 能 +CHINESE_GLYPH(0x8272) // 色 +CHINESE_GLYPH(0x82B1) // 花 +CHINESE_GLYPH(0x89C6) // 视 +CHINESE_GLYPH(0x89E6) // 触 +CHINESE_GLYPH(0x95F4) // 间 +CHINESE_GLYPH(0x968F) // 随 +CHINESE_GLYPH(0x9694) // 隔 +CHINESE_GLYPH(0x987A) // 顺 +CHINESE_GLYPH(0x989D) // 额 +CHINESE_GLYPH(0x9ED1) // 黑 +CHINESE_GLYPH(0x3001) // 、 diff --git a/include/gbalatro_sys8.h b/include/gbalatro_sys8.h index c16c6d0..8f3486f 100644 --- a/include/gbalatro_sys8.h +++ b/include/gbalatro_sys8.h @@ -1,9 +1,9 @@ -#ifndef GBALATRO_SYS8_FONT_H -#define GBALATRO_SYS8_FONT_H - -#include - -extern const TFont gbalatro_sys8Font; -extern const unsigned int gbalatro_sys8Glyphs[192]; - -#endif \ No newline at end of file +#ifndef GBALATRO_SYS8_H +#define GBALATRO_SYS8_H + +#include + +extern const TFont gbalatro_sys8Font; +extern const unsigned int gbalatro_sys8Glyphs[]; + +#endif diff --git a/include/graphic_utils.h b/include/graphic_utils.h index 9c23797..046804a 100644 --- a/include/graphic_utils.h +++ b/include/graphic_utils.h @@ -32,7 +32,7 @@ #define MAIN_BG_CBB 1 #define TTE_SBB 30 #define TTE_CBB 0 -#define AFFINE_BG_SBB 2 +#define AFFINE_BG_SBB 4 #define AFFINE_BG_CBB 2 #define PAL_ROW_LEN 16 #define NUM_PALETTES 16 diff --git a/scripts/generate_chinese_joker_font.py b/scripts/generate_chinese_joker_font.py new file mode 100644 index 0000000..37975a4 --- /dev/null +++ b/scripts/generate_chinese_joker_font.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +"""Append the Chinese Joker-description glyph subset to the project's 8x8 ASCII font.""" + +from argparse import ArgumentParser +from pathlib import Path +import re + +from PIL import Image, ImageDraw, ImageFont + +ASCII_FONT_HEIGHT = 48 +CELL_SIZE = 8 +GLYPHS_PER_ROW = 16 + + +def read_codepoints(table_path: Path) -> list[int]: + table = table_path.read_text(encoding="utf-8") + return [int(value, 16) for value in re.findall(r"CHINESE_GLYPH\(0x([0-9A-Fa-f]+)\)", table)] + + +def main() -> None: + parser = ArgumentParser() + parser.add_argument("--font", required=True, help="Fusion Pixel Font 8px zh_hans TTF") + parser.add_argument("--table", default="include/def_chinese_glyph_table.h") + parser.add_argument("--image", default="font/gbalatro_sys8.png") + args = parser.parse_args() + + image_path = Path(args.image) + base = Image.open(image_path).convert("RGB").crop((0, 0, 128, ASCII_FONT_HEIGHT)) + codepoints = read_codepoints(Path(args.table)) + extra_rows = (len(codepoints) + GLYPHS_PER_ROW - 1) // GLYPHS_PER_ROW + + output = Image.new("RGB", (128, ASCII_FONT_HEIGHT + extra_rows * CELL_SIZE), "white") + output.paste(base, (0, 0)) + + draw = ImageDraw.Draw(output) + font = ImageFont.truetype(args.font, CELL_SIZE) + for index, codepoint in enumerate(codepoints): + x = (index % GLYPHS_PER_ROW) * CELL_SIZE + y = ASCII_FONT_HEIGHT + (index // GLYPHS_PER_ROW) * CELL_SIZE + draw.text((x, y - 1), chr(codepoint), font=font, fill="black") + + output.save(image_path, optimize=True) + print(f"Appended {len(codepoints)} Chinese glyphs to {image_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_font.py b/scripts/generate_font.py index 1d4cc89..1eac474 100644 --- a/scripts/generate_font.py +++ b/scripts/generate_font.py @@ -5,7 +5,6 @@ import argparse CHAR_WIDTH = 8 CHAR_HEIGHT = 8 -NUM_VERT_CHARS = 6 NUM_HORZ_CHARS = 16 WORD_SIZE = 32 @@ -48,8 +47,14 @@ img = img.convert("P") img = img.convert('1') img = ImageOps.invert(img) +if img.width != NUM_HORZ_CHARS * CHAR_WIDTH or img.height % CHAR_HEIGHT != 0: + raise ValueError("font image must be 128px wide and a multiple of 8px high") + +num_vert_chars = img.height // CHAR_HEIGHT +num_chars = num_vert_chars * NUM_HORZ_CHARS + glyphs = [] -for char_y in range(NUM_VERT_CHARS): +for char_y in range(num_vert_chars): for char_x in range(NUM_HORZ_CHARS): # Get the 8x8 crop area at our current character in the image pix_x = char_x * CHAR_WIDTH @@ -86,7 +91,7 @@ with open(out_path, "w") as out: .global gbalatro_sys8Font gbalatro_sys8Font: .word gbalatro_sys8Glyphs, 0, 0 - .hword 32, 96 + .hword 32, %d .byte 8, 8 .byte 8, 8 .hword 8 @@ -94,9 +99,9 @@ gbalatro_sys8Font: .section .rodata .align 2 - .global gbalatro_sys8Glyphs @ 768 bytes (192 unsigned ints) + .global gbalatro_sys8Glyphs gbalatro_sys8Glyphs: -""") +""" % num_chars) for i, glyph in enumerate(glyphs): if first: diff --git a/source/chinese_text.c b/source/chinese_text.c new file mode 100644 index 0000000..71568d8 --- /dev/null +++ b/source/chinese_text.c @@ -0,0 +1,113 @@ +#include "chinese_text.h" + +#include + +#define CHINESE_GLYPH_FIRST 128 + +static const uint32_t s_chinese_codepoints[] = { +#define CHINESE_GLYPH(codepoint) codepoint, +#include "def_chinese_glyph_table.h" +#undef CHINESE_GLYPH +}; + +_Static_assert( + sizeof(s_chinese_codepoints) / sizeof(s_chinese_codepoints[0]) <= 256 - CHINESE_GLYPH_FIRST, + "Chinese glyph table exceeds the single-byte encoding range" +); + +static uint32_t decode_utf8_codepoint(const unsigned char** cursor) +{ + const unsigned char* text = *cursor; + + if (text[0] < 0x80) + { + *cursor += 1; + return text[0]; + } + + if ((text[0] & 0xE0) == 0xC0 && (text[1] & 0xC0) == 0x80) + { + *cursor += 2; + return ((uint32_t)(text[0] & 0x1F) << 6) | (text[1] & 0x3F); + } + + if ((text[0] & 0xF0) == 0xE0 && (text[1] & 0xC0) == 0x80 && (text[2] & 0xC0) == 0x80) + { + *cursor += 3; + return ((uint32_t)(text[0] & 0x0F) << 12) | ((uint32_t)(text[1] & 0x3F) << 6) | + (text[2] & 0x3F); + } + + *cursor += 1; + return '?'; +} + +static uint32_t codepoint_to_glyph(uint32_t codepoint) +{ + for (size_t i = 0; i < sizeof(s_chinese_codepoints) / sizeof(s_chinese_codepoints[0]); i++) + { + if (s_chinese_codepoints[i] == codepoint) + { + return CHINESE_GLYPH_FIRST + i; + } + } + + return '?'; +} + +static size_t encode_tte_codepoint(uint32_t codepoint, char* output, size_t output_size) +{ + if (codepoint < 0x80) + { + if (output_size < 1) + { + return 0; + } + + output[0] = (char)codepoint; + return 1; + } + + if (codepoint <= 0x7FF) + { + if (output_size < 2) + { + return 0; + } + + output[0] = (char)(0xC0 | (codepoint >> 6)); + output[1] = (char)(0x80 | (codepoint & 0x3F)); + return 2; + } + + return 0; +} + +size_t chinese_text_encode(const char* utf8, char* output, size_t output_size) +{ + if (utf8 == NULL || output == NULL || output_size == 0) + { + return 0; + } + + const unsigned char* cursor = (const unsigned char*)utf8; + size_t output_len = 0; + + while (*cursor != '\0') + { + uint32_t codepoint = decode_utf8_codepoint(&cursor); + uint32_t tte_codepoint = codepoint < 0x80 ? codepoint : codepoint_to_glyph(codepoint); + size_t encoded_size = + encode_tte_codepoint(tte_codepoint, output + output_len, output_size - output_len - 1); + + if (encoded_size == 0) + { + break; + } + + output_len += encoded_size; + } + + output[output_len] = '\0'; + return output_len; +} diff --git a/source/main.c b/source/main.c index 9c1fb3e..4e52e37 100644 --- a/source/main.c +++ b/source/main.c @@ -25,6 +25,31 @@ #include "soundbank.h" #include "soundbank_bin.h" +enum +{ + TTE_ASCII_GLYPH_COUNT = 96, + FONT_TILES_PER_ROW = 16, + CHARBLOCK_4BPP_TILE_COUNT = 512, + SCREENBLOCK_4BPP_TILE_COUNT = 64, + CHINESE_GLYPH_COUNT = 0 +#define CHINESE_GLYPH(codepoint) +1 +#include "def_chinese_glyph_table.h" +#undef CHINESE_GLYPH + , + TTE_FONT_TILE_COUNT = + TTE_ASCII_GLYPH_COUNT + + ((CHINESE_GLYPH_COUNT + FONT_TILES_PER_ROW - 1) / FONT_TILES_PER_ROW) * FONT_TILES_PER_ROW, + TTE_FONT_FIRST_TILE = TTE_CBB * CHARBLOCK_4BPP_TILE_COUNT, + TTE_FONT_END_TILE = TTE_FONT_FIRST_TILE + TTE_FONT_TILE_COUNT, + AFFINE_BG_MAP_FIRST_TILE = AFFINE_BG_SBB * SCREENBLOCK_4BPP_TILE_COUNT, + AFFINE_BG_MAP_END_TILE = AFFINE_BG_MAP_FIRST_TILE + SCREENBLOCK_4BPP_TILE_COUNT, +}; + +_Static_assert( + TTE_FONT_END_TILE <= AFFINE_BG_MAP_FIRST_TILE || AFFINE_BG_MAP_END_TILE <= TTE_FONT_FIRST_TILE, + "TTE font tiles overlap the affine background map" +); + void init() { rng_init(); diff --git a/tests/chinese_text/Makefile b/tests/chinese_text/Makefile new file mode 100644 index 0000000..ef46e43 --- /dev/null +++ b/tests/chinese_text/Makefile @@ -0,0 +1,15 @@ +CC := gcc +CFLAGS := -I../../include -I. \ + -g -O3 -std=gnu23 -Wall -Werror +SRC := chinese_text_test.c \ + ../../source/chinese_text.c +OUT := build/chinese_text_test + +$(OUT): $(SRC) | build + $(CC) $(CFLAGS) -o $@ $^ + +build: + mkdir -p build + +clean: + rm -f $(OUT) diff --git a/tests/chinese_text/chinese_text_test.c b/tests/chinese_text/chinese_text_test.c new file mode 100644 index 0000000..c74cf65 --- /dev/null +++ b/tests/chinese_text/chinese_text_test.c @@ -0,0 +1,120 @@ +#include "chinese_text.h" + +#include +#include +#include +#include + +static const uint32_t CHINESE_CODEPOINTS[] = { +#define CHINESE_GLYPH(codepoint) codepoint, +#include "def_chinese_glyph_table.h" +#undef CHINESE_GLYPH +}; + +static size_t encode_utf8_codepoint(uint32_t codepoint, char output[4]) +{ + if (codepoint <= 0x7F) + { + output[0] = (char)codepoint; + return 1; + } + if (codepoint <= 0x7FF) + { + output[0] = (char)(0xC0 | (codepoint >> 6)); + output[1] = (char)(0x80 | (codepoint & 0x3F)); + return 2; + } + + output[0] = (char)(0xE0 | (codepoint >> 12)); + output[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); + output[2] = (char)(0x80 | (codepoint & 0x3F)); + return 3; +} + +static uint32_t decode_like_tonc(const unsigned char encoded[2]) +{ + return ((uint32_t)(encoded[0] & 0x1F) << 6) | (encoded[1] & 0x3F); +} + +static void test_ascii_is_unchanged(void) +{ + char output[32]; + + size_t output_len = chinese_text_encode("A: +20", output, sizeof(output)); + + assert(output_len == strlen("A: +20")); + assert(strcmp(output, "A: +20") == 0); +} + +static void test_supported_chinese_uses_extended_glyphs(void) +{ + char output[8]; + + size_t output_len = chinese_text_encode("小丑牌", output, sizeof(output)); + + assert(output_len == 6); + assert((unsigned char)output[0] == 0xC2); + assert((unsigned char)output[1] == 0x80); + assert((unsigned char)output[2] == 0xC2); + assert((unsigned char)output[3] == 0x81); + assert((unsigned char)output[4] == 0xC2); + assert((unsigned char)output[5] == 0x82); + assert(output[6] == '\0'); +} + +static void test_unsupported_unicode_becomes_question_mark(void) +{ + char output[8]; + + size_t output_len = chinese_text_encode("龘", output, sizeof(output)); + + assert(output_len == 1); + assert(strcmp(output, "?") == 0); +} + +static void test_all_custom_glyphs_round_trip_through_tonc_utf8(void) +{ + size_t glyph_count = sizeof(CHINESE_CODEPOINTS) / sizeof(CHINESE_CODEPOINTS[0]); + assert(glyph_count <= 128); + + for (size_t i = 0; i < glyph_count; i++) + { + char input[4] = {'\0'}; + size_t input_len = encode_utf8_codepoint(CHINESE_CODEPOINTS[i], input); + input[input_len] = '\0'; + + char output[4] = {'\0'}; + size_t output_len = chinese_text_encode(input, output, sizeof(output)); + uint32_t expected_glyph_codepoint = 128 + i; + + assert(output_len == 2); + assert(((unsigned char)output[0] & 0xE0) == 0xC0); + assert(((unsigned char)output[1] & 0xC0) == 0x80); + assert(decode_like_tonc((const unsigned char*)output) == expected_glyph_codepoint); + assert(output[2] == '\0'); + } +} + +static void test_small_buffer_does_not_split_utf8_sequence(void) +{ + char output[4] = {'x', 'x', 'x', 'x'}; + + size_t output_len = chinese_text_encode("小丑牌", output, sizeof(output)); + + assert(output_len == 2); + assert((unsigned char)output[0] == 0xC2); + assert((unsigned char)output[1] == 0x80); + assert(output[2] == '\0'); +} + +int main(void) +{ + test_ascii_is_unchanged(); + test_supported_chinese_uses_extended_glyphs(); + test_unsupported_unicode_becomes_question_mark(); + test_all_custom_glyphs_round_trip_through_tonc_utf8(); + test_small_buffer_does_not_split_utf8_sequence(); + + printf("Chinese text tests passed.\n"); + return 0; +} diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 9b24152..635e000 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -17,6 +17,7 @@ run_test() { } run_test bitset +run_test chinese_text run_test list run_test pool run_test util