diff --git a/butano/include/bn_documentation.h b/butano/include/bn_documentation.h index 20458c47d..f80d70402 100644 --- a/butano/include/bn_documentation.h +++ b/butano/include/bn_documentation.h @@ -924,6 +924,12 @@ * * Showcase of a 4096x4096 world map with a perspective effect. * + * @ref font + * + * @image html examples_font.png + * + * Showcase of Butano BMFont support. + * * */ @@ -1326,6 +1332,53 @@ * * bn::sound_items::sfx.play(); * @endcode + * + * + * @section import_fonts Fonts + * + * By default fonts files go into the `fonts` folder of your project. + * + * + * @subsection import_bmfont Bitmap Font + * + * A bitmap font file (.fnt) in text format is supported. You can make it using Bitmap Font Generator, Bitmap Font Generator Online or BMfont-web. + * + * It is also a common font format used in generic game engines like Cocos, Unity and visual novel engines like Ren'Py. + * + * First of all, install Pillow. + * + * @code{.sh} + * # For MSYS2/MinGW-w64 users + * pacman -S mingw-w64-x86_64-python-pillow + * # For WSL2/Ubuntu/Debian users + * sudo apt-get install python3-pil + * # For Mac users + * brew install pillow + * # For FreeBSD users + * pkg install py38-pillow + * # For CentOS users + * yum install python3-pillow + * # For Fedora Linux users + * dnf install python3-pillow + * # For Arch Linux users + * pacman -S python37-pillow + * @endcode + * + * Place `*.fnt` and `*.png` files in the `fonts` folder. Notice that `*.png` files should have background color instead of transparent background. + * + * If the conversion process has finished successfully, + * a bunch of bn::sprite_font objects should have been generated in the `build` folder for all fonts files. + * + * For example, from two files named `items.fnt` and `items.png`, + * a header file named `items_sprite_font.h` is generated in the `build` folder. + * + * You can use these fonts to display text with only one line of C++ code: + * + * @code{.cpp} + * #include "items_sprite_font.h" + * + * bn::sprite_text_generator text_generator(items_sprite_font); + * @endcode */ @@ -1658,6 +1711,7 @@ * but the bn::sprite_font instances used in the examples don't provide japanese nor chinese characters, * so you will have to make a new one with them. * + * You can get ready-to-use free fonts here. * * @subsection faq_tonc_general_notes Are there some more general notes on GBA programming out there? * diff --git a/butano/include/bn_utf8_characters_map_ref.h b/butano/include/bn_utf8_characters_map_ref.h index 70d0065d9..8621ed953 100644 --- a/butano/include/bn_utf8_characters_map_ref.h +++ b/butano/include/bn_utf8_characters_map_ref.h @@ -103,7 +103,7 @@ class utf8_characters_map_ref ++its; } - BN_ERROR("UTF-8 character not found"); + BN_ERROR("UTF-8 character not found: ", key); return 0; } diff --git a/butano/tools/butano_fonts_tool.py b/butano/tools/butano_fonts_tool.py new file mode 100644 index 000000000..c1427d5f1 --- /dev/null +++ b/butano/tools/butano_fonts_tool.py @@ -0,0 +1,263 @@ +""" +Copyright (c) 2022 laqieer laqieer@126.com +zlib License, see LICENSE file. +""" + +# Doc on BMFont: https://angelcode.com/products/bmfont/doc/file_format.html + +import os +import sys +import shlex +import codecs +import argparse + +from file_info import FileInfo +from PIL import Image + +trim_fonts = False +unique_characters = {' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'} + + +def list_texts_files(texts_paths): + global trim_fonts + texts_file_names = [] + texts_file_paths = [] + + if texts_paths is not None: + texts_path_list = texts_paths.split(' ') + + for texts_path in texts_path_list: + if os.path.isfile(texts_path): + texts_file_name = os.path.basename(texts_path) + if FileInfo.validate(texts_file_name): + texts_file_names.append(texts_file_name) + texts_file_paths.append(texts_path) + elif os.path.isdir(texts_path): + folder_texts_file_names = sorted(os.listdir(texts_path)) + for texts_file_name in folder_texts_file_names: + texts_file_path = texts_path + '/' + texts_file_name + + if os.path.isfile(texts_file_path) and FileInfo.validate(texts_file_name): + texts_file_names.append(texts_file_name) + texts_file_paths.append(texts_file_path) + + trim_fonts = len(texts_file_names) > 0 + + return texts_file_names, texts_file_paths + + +def process_texts_files(texts_file_paths): + global unique_characters + for texts_file_path in texts_file_paths: + with open(texts_file_path, 'r', encoding='UTF-8') as texts_file: + for line in texts_file: + for char in line: + unique_characters.add(char) + + +def list_fonts_files(fonts_folder_paths): + fonts_folder_path_list = fonts_folder_paths.split(' ') + fonts_file_names = [] + fonts_file_paths = [] + + for fonts_folder_path in fonts_folder_path_list: + folder_fonts_file_names = sorted(os.listdir(fonts_folder_path)) + + for fonts_file_name in folder_fonts_file_names: + if fonts_file_name.endswith('.fnt'): + fonts_file_path = fonts_folder_path + '/' + fonts_file_name + + if os.path.isfile(fonts_file_path) and FileInfo.validate(fonts_file_name): + fonts_file_names.append(fonts_file_name) + fonts_file_paths.append(fonts_file_path) + + return fonts_file_names, fonts_file_paths + + +def process_fonts_files(fonts_file_paths, build_folder_path): + global trim_fonts, unique_characters + fonts_graphics_path = build_folder_path + '/fonts/' + if not os.path.exists(fonts_graphics_path): + os.makedirs(fonts_graphics_path) + total_number = 0 + for fonts_file_path in fonts_file_paths: + fonts_file_path_no_ext = os.path.splitext(fonts_file_path)[0] + fonts_folder_path, fonts_file_name_no_ext = os.path.split(fonts_file_path_no_ext) + font_name = fonts_file_name_no_ext + '_sprite_font' + fonts_header_path = build_folder_path + '/' + font_name + '.h' + + with open(fonts_file_path, 'r') as fonts_file, open(fonts_header_path, 'w', encoding='utf-8') as header_file: + font_chars = [] + font_widths = [0] * 95 + unique_chars = unique_characters.copy() + + for fonts_line in fonts_file: + line_type, *pair_tokens = shlex.split(fonts_line) + line_conf = dict(pair_token.split("=", 1) for pair_token in pair_tokens) + + if line_type == "info": + padding_up, padding_right, padding_down, padding_left = [int(x) for x in line_conf['padding'].split(',')] + header_file.write('// ' + line_conf['face'].replace('"', '') + ' ' + line_conf['size'] + 'px') + if line_conf['bold'] != '0': + header_file.write(' bold') + if line_conf['italic'] != '0': + header_file.write(' italic') + header_file.write('\n') + header_file.write('\n') + elif line_type == "common": + font_height = int(line_conf['lineHeight']) + if font_height > 64: + raise ValueError('Font is too large') + elif font_height > 32: + font_height = 64 + elif font_height > 16: + font_height = 32 + elif font_height > 8: + font_height = 16 + else: + font_height = 8 + font_base = int(line_conf['base']) + font_y_offset = min(font_base - font_height, 0) + # Assume a font's width is not more than its height + font_width = font_height + font_pages = [None] * int(line_conf['pages']) + elif line_type == "page": + page_file_path = fonts_folder_path + '/' + line_conf['file'].replace('"', '') + font_pages[int(line_conf['id'])] = Image.open(page_file_path) + elif line_type == "chars": + font_number = int(line_conf['count']) + #fonts_image = Image.new('RGBA', (font_width, font_height * font_number)) + transparent_color = font_pages[0].getpixel((0, 0)) + fonts_image = Image.new('RGB', (font_width, font_height * (font_number + 94)), transparent_color) + elif line_type == "char": + if len(unique_characters) == 0: + break + font_code = int(line_conf['id']) + if not trim_fonts or chr(font_code) in unique_chars: + if trim_fonts: + unique_chars.remove(chr(font_code)) + src_left = int(line_conf['x']) + padding_left + src_upper = int(line_conf['y']) + padding_up + src_right = int(line_conf['x']) + int(line_conf['width']) - padding_right + if src_right < src_left: + src_right = src_left + src_right = min(src_right, src_left + font_width) + src_lower = int(line_conf['y']) + int(line_conf['height']) - padding_down + if src_lower < src_upper: + src_lower = src_upper + src_upper = max(src_upper, src_lower - font_height) + dst_left = round(float(line_conf['xoffset'])) + padding_left + if dst_left < 0: + dst_left = 0 + dst_right = dst_left + src_right - src_left + if dst_right > font_width: + dst_left -= min(dst_left, dst_right - font_width) + dst_upper = round(float(line_conf['yoffset'])) + padding_up + if dst_upper < 0: + dst_upper = 0 + dst_lower = dst_upper + src_lower - src_upper + if dst_lower > font_height: + dst_upper -= min(dst_upper, dst_lower - font_height) + if dst_lower > 0: + dst_lower -= min(dst_lower, font_y_offset) + font_w = max(int(line_conf['xadvance']), int(line_conf['width']) - padding_left -padding_right) + font_w = min(font_w, font_width) + if font_code > 126: + font_chars.append(chr(font_code)) + font_widths.append(font_w) + dst_upper += font_height * (len(font_widths) - 2) + elif font_code > 31: + font_widths[font_code - 32] = font_w + dst_upper += font_height * (font_code - 33) + if font_code > 32: + fonts_image.paste(font_pages[int(line_conf['page'])].crop((src_left, src_upper, src_right, src_lower)), (dst_left, dst_upper)) + + header_file.write('#ifndef ' + font_name.upper() + '_H\n') + header_file.write('#define ' + font_name.upper() + '_H\n') + header_file.write('\n') + header_file.write('#include "bn_sprite_font.h"\n') + header_file.write('#include "bn_utf8_characters_map.h"\n') + header_file.write('#include "bn_sprite_items_' + fonts_file_name_no_ext + '.h"\n') + header_file.write('\n') + header_file.write('constexpr bn::utf8_character ' + font_name + '_utf8_characters[] = {\n') + header_file.write(' "' + '", "'.join(font_chars) + '"\n') + header_file.write('};\n') + header_file.write('\n') + header_file.write('constexpr bn::span ' + font_name + '_utf8_characters_span(\n') + header_file.write(' ' + font_name + '_utf8_characters);\n') + header_file.write('\n') + header_file.write('constexpr auto ' + font_name + '_utf8_characters_map =\n') + header_file.write(' bn::utf8_characters_map<' + font_name + '_utf8_characters_span>();\n') + header_file.write('\n') + header_file.write('constexpr int8_t ' + font_name + '_character_widths[] = {\n') + header_file.write(' ' + ', '.join([str(x) for x in font_widths]) + '\n') + header_file.write('};\n') + header_file.write('\n') + header_file.write('constexpr bn::sprite_font ' + font_name + '(\n') + header_file.write(' bn::sprite_items::' + fonts_file_name_no_ext + ',\n') + header_file.write(' ' + font_name + '_utf8_characters_map.reference(),\n') + header_file.write(' ' + font_name + '_character_widths);\n') + header_file.write('\n') + header_file.write('#endif') + fonts_image_path_no_ext = fonts_graphics_path + fonts_file_name_no_ext + font_number = len(font_widths) - 1 + total_number += font_number + fonts_image_trimmed = Image.new('RGB', (font_width, font_height * total_number), transparent_color) + fonts_image_trimmed.paste(fonts_image) + #fonts_image_trimmed.save(fonts_image_path_no_ext + '.png') + fonts_image_trimmed = fonts_image_trimmed.convert("P", palette=Image.ADAPTIVE, colors=16) + transparent_color_index = fonts_image_trimmed.getpixel((0, 0)) + if transparent_color_index > 0: + dest_map = list(range(16)) + dest_map[0], dest_map[transparent_color_index] = transparent_color_index, 0 + fonts_image_trimmed = fonts_image_trimmed.remap_palette(dest_map) + fonts_image_trimmed.save(fonts_image_path_no_ext + '.bmp') + with open(fonts_image_path_no_ext + '.json', 'w') as json_file: + json_file.write('{\n') + json_file.write(' "type": "sprite",\n') + json_file.write(' "height": ' + str(font_height) + '\n') + json_file.write('}\n') + print(' ' + fonts_file_path + ' font header written in ' + fonts_header_path + ' (character number: ' + str(font_number) + ')') + + return total_number + + +def process_fonts(fonts_folder_paths, build_folder_path, texts_paths): + texts_file_names, texts_file_paths = list_texts_files(texts_paths) + text_file_info_path = build_folder_path + '/_bn_texts_files_info.txt' + old_text_file_info = FileInfo.read(text_file_info_path) + new_text_file_info = FileInfo.build_from_files(texts_file_paths) + + fonts_file_names, fonts_file_paths = list_fonts_files(fonts_folder_paths) + font_file_info_path = build_folder_path + '/_bn_fonts_files_info.txt' + old_font_file_info = FileInfo.read(font_file_info_path) + new_font_file_info = FileInfo.build_from_files(fonts_file_paths) + + if old_font_file_info == new_font_file_info and old_text_file_info == new_text_file_info: + return + + for fonts_file_name in fonts_file_names: + print(fonts_file_name) + + sys.stdout.flush() + + process_texts_files(texts_file_paths) + total_number = process_fonts_files(fonts_file_paths, build_folder_path) + print(' Processed character number: ' + str(total_number)) + new_font_file_info.write(font_file_info_path) + new_text_file_info.write(text_file_info_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Butano fonts tool.') + parser.add_argument('--build', required=True, help='build folder path') + parser.add_argument('--fonts', required=True, help='fonts folder paths') + parser.add_argument('--texts', required=False, help='texts folder or files paths') + + try: + args = parser.parse_args() + process_fonts(args.fonts, args.build, args.texts) + except Exception as ex: + sys.stderr.write('Error: ' + str(ex) + '\n') + traceback.print_exc() + exit(-1) diff --git a/credits/fonts.txt b/credits/fonts.txt new file mode 100644 index 000000000..c4300ccea --- /dev/null +++ b/credits/fonts.txt @@ -0,0 +1,5 @@ +- Source Han Sans + By Adobe, Google + Licensed under the SIL Open Font License v.1.1 + https://commons.wikimedia.org/wiki/File:Source_Han_Sans_Version_2_Specimen.svg + diff --git a/docs/examples_font.png b/docs/examples_font.png new file mode 100644 index 000000000..e03dc3ecc Binary files /dev/null and b/docs/examples_font.png differ diff --git a/examples/font/Makefile b/examples/font/Makefile new file mode 100644 index 000000000..ffb7b08f6 --- /dev/null +++ b/examples/font/Makefile @@ -0,0 +1,59 @@ +#--------------------------------------------------------------------------------------------------------------------- +# TARGET is the name of the output. +# BUILD is the directory where object files & intermediate files will be placed. +# LIBBUTANO is the main directory of butano library (https://github.com/GValiente/butano). +# PYTHON is the path to the python interpreter. +# SOURCES is a list of directories containing source code. +# INCLUDES is a list of directories containing extra header files. +# DATA is a list of directories containing binary data. +# GRAPHICS is a list of directories containing files to be processed by grit. +# AUDIO is a list of directories containing files to be processed by mmutil. +# FONTS is a list of directories containing font files. +# ROMTITLE is a uppercase ASCII, max 12 characters text string containing the output ROM title. +# ROMCODE is a uppercase ASCII, max 4 characters text string containing the output ROM code. +# USERFLAGS is a list of additional compiler flags: +# Pass -flto to enable link-time optimization. +# Pass -O0 to improve debugging. +# USERLIBDIRS is a list of additional directories containing libraries. +# Each libraries directory must contains include and lib subdirectories. +# USERLIBS is a list of additional libraries to link with the project. +# USERBUILD is a list of additional directories to remove when cleaning the project. +# EXTTOOL is an optional command executed before processing audio, graphics and code files. +# +# All directories are specified relative to the project directory where the makefile is found. +#--------------------------------------------------------------------------------------------------------------------- +TARGET := $(notdir $(CURDIR)) +BUILD := build +LIBBUTANO := ../../butano +PYTHON := python +SOURCES := src +INCLUDES := include +DATA := +GRAPHICS := graphics +AUDIO := audio +FONTS := fonts +ROMTITLE := BUTANO FONT +ROMCODE := SBTP +USERFLAGS := +USERLIBDIRS := +USERLIBS := +USERBUILD := +EXTTOOL := + +ifneq ($(strip $(FONTS)),) +INCLUDES += $(BUILD) +GRAPHICS += $(BUILD)/fonts +EXTTOOL += $(PYTHON) $(LIBBUTANO)/tools/butano_fonts_tool.py --build=$(BUILD) --fonts="$(FONTS)" +endif + +#--------------------------------------------------------------------------------------------------------------------- +# Export absolute butano path: +#--------------------------------------------------------------------------------------------------------------------- +ifndef LIBBUTANOABS + export LIBBUTANOABS := $(realpath $(LIBBUTANO)) +endif + +#--------------------------------------------------------------------------------------------------------------------- +# Include main makefile: +#--------------------------------------------------------------------------------------------------------------------- +include $(LIBBUTANOABS)/butano.mak diff --git a/examples/font/audio/.gitignore b/examples/font/audio/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/examples/font/fonts/user_variable_32x32.fnt b/examples/font/fonts/user_variable_32x32.fnt new file mode 100644 index 000000000..1ffb458cb --- /dev/null +++ b/examples/font/fonts/user_variable_32x32.fnt @@ -0,0 +1,40 @@ +info face="SourceHanSansSCVF-ExtraLight" size=30 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=1,1,1,1 spacing=1,1 +common lineHeight=30 base=30 scaleW=152 scaleH=149 pages=1 packed=0 +page id=0 file="user_variable_32x32.png" +chars count=22 +char id=32 x=0 y=0 width=0 height=0 xoffset=0 yoffset=0 xadvance=7 page=0 chnl=15 +char id=12290 x=0 y=135 width=14 height=14 xoffset=-0.08800000000000008 yoffset=20.96 xadvance=30 page=0 chnl=15 +char id=12377 x=69 y=32 width=31 height=30 xoffset=1.024 yoffset=4.932 xadvance=30 page=0 chnl=15 +char id=12391 x=69 y=63 width=31 height=28 xoffset=0.9279999999999999 yoffset=7.018000000000001 xadvance=30 page=0 chnl=15 +char id=12398 x=101 y=0 width=30 height=27 xoffset=1.072 yoffset=7.042 xadvance=30 page=0 chnl=15 +char id=12457 x=101 y=117 width=26 height=25 xoffset=4 yoffset=9.994000000000002 xadvance=30 page=0 chnl=15 +char id=12470 x=35 y=102 width=32 height=31 xoffset=-0.05800000000000005 yoffset=4.938000000000001 xadvance=30 page=0 chnl=15 +char id=12488 x=132 y=0 width=20 height=30 xoffset=8.93 yoffset=5.058 xadvance=30 page=0 chnl=15 +char id=12501 x=101 y=90 width=26 height=26 xoffset=2.96 yoffset=7.914 xadvance=30 page=0 chnl=15 +char id=12518 x=69 y=92 width=31 height=23 xoffset=0.9220000000000002 yoffset=8.912 xadvance=30 page=0 chnl=15 +char id=12531 x=101 y=28 width=28 height=27 xoffset=3.002 yoffset=6.970000000000001 xadvance=30 page=0 chnl=15 +char id=12540 x=35 y=134 width=30 height=7 xoffset=1.072 yoffset=15.982 xadvance=30 page=0 chnl=15 +char id=20041 x=35 y=0 width=33 height=33 xoffset=-0.9359999999999999 yoffset=2.984 xadvance=30 page=0 chnl=15 +char id=20307 x=0 y=34 width=34 height=33 xoffset=-0.9180000000000001 yoffset=3.026 xadvance=30 page=0 chnl=15 +char id=23383 x=35 y=68 width=32 height=33 xoffset=0.04999999999999982 yoffset=2.9779999999999998 xadvance=30 page=0 chnl=15 +char id=23450 x=0 y=0 width=34 height=33 xoffset=-0.9180000000000001 yoffset=3.002 xadvance=30 page=0 chnl=15 +char id=25143 x=69 y=116 width=30 height=33 xoffset=-0.948 yoffset=2.9899999999999993 xadvance=30 page=0 chnl=15 +char id=26159 x=0 y=68 width=34 height=32 xoffset=-0.9119999999999999 yoffset=4.908 xadvance=30 page=0 chnl=15 +char id=29992 x=69 y=0 width=31 height=31 xoffset=-0.954 yoffset=5.058 xadvance=30 page=0 chnl=15 +char id=33258 x=101 y=56 width=26 height=33 xoffset=3.0919999999999996 yoffset=3.002 xadvance=30 page=0 chnl=15 +char id=35373 x=35 y=34 width=33 height=33 xoffset=-0.06400000000000006 yoffset=2.9959999999999996 xadvance=30 page=0 chnl=15 +char id=36825 x=0 y=101 width=33 height=33 xoffset=-0.10000000000000009 yoffset=2.9959999999999996 xadvance=30 page=0 chnl=15 +kernings count=13 +kerning first=12540 second=12470 amount=-1 +kerning first=12540 second=12501 amount=-2 +kerning first=12540 second=12531 amount=-1 +kerning first=12540 second=12391 amount=-2 +kerning first=12470 second=12290 amount=-2 +kerning first=12501 second=12290 amount=-1 +kerning first=12501 second=12531 amount=1 +kerning first=12457 second=12290 amount=-1 +kerning first=12531 second=12290 amount=-2 +kerning first=12531 second=12501 amount=-1 +kerning first=12488 second=12290 amount=-1 +kerning first=12391 second=12290 amount=-1 +kerning first=12377 second=12290 amount=-3 diff --git a/examples/font/fonts/user_variable_32x32.png b/examples/font/fonts/user_variable_32x32.png new file mode 100644 index 000000000..02694634a Binary files /dev/null and b/examples/font/fonts/user_variable_32x32.png differ diff --git a/examples/font/graphics/.gitignore b/examples/font/graphics/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/examples/font/include/.gitignore b/examples/font/include/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/examples/font/src/main.cpp b/examples/font/src/main.cpp new file mode 100644 index 000000000..219c8414f --- /dev/null +++ b/examples/font/src/main.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020-2022 Gustavo Valiente gustavo.valiente@protonmail.com + * zlib License, see LICENSE file. + */ + +#include "bn_core.h" +#include "bn_math.h" +#include "bn_keypad.h" +#include "bn_sprite_ptr.h" +#include "bn_bg_palettes.h" +#include "bn_string_view.h" +#include "bn_sprite_text_generator.h" + +#include "user_variable_32x32_sprite_font.h" + +namespace +{ + void user_font_text_scene() + { + bn::sprite_text_generator user_font_text_generator(user_variable_32x32_sprite_font); + user_font_text_generator.set_center_alignment(); + + bn::vector user_font_text_sprites; + user_font_text_generator.generate(0, -48, "这是用户自定义", user_font_text_sprites); + user_font_text_generator.generate(0, -16, "字体。", user_font_text_sprites); + user_font_text_generator.generate(0, 16, "ユーザー設定の", user_font_text_sprites); + user_font_text_generator.generate(0, 48, "フォントです。", user_font_text_sprites); + + while(! bn::keypad::start_pressed()) + { + bn::core::update(); + } + } +} + +int main() +{ + bn::core::init(); + + bn::bg_palettes::set_transparent_color(bn::color(16, 16, 16)); + + while(true) + { + user_font_text_scene(); + bn::core::update(); + } +}