Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature: BMFont support #18

Closed
wants to merge 47 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
70e159b
butano: support BMFont (tool, template, example)
laqieer Feb 8, 2022
3f636af
butano: support BMFont (makefile fixed)
laqieer Feb 8, 2022
0ab26d8
butano: common font for Chinese and Japanese
laqieer Feb 9, 2022
c1b8906
butano: allow to use common Chinese or Japanese font without BMFont
laqieer Feb 9, 2022
b061a33
fix: Makefile for BMFont support
laqieer Feb 9, 2022
d30da78
fix: conv script for BMFont
laqieer Feb 9, 2022
43df41a
examples: update user font for text example
laqieer Feb 9, 2022
93bca7e
butano: add config `BN_CFG_SPRITE_TEXT_MAX_UTF8_CHARACTERS_FOR_DUPLIC…
laqieer Feb 9, 2022
20c71e9
documentation: guide on how to use common chinese or japanese charact…
laqieer Feb 9, 2022
d88f786
fix: increase max times limit on constexpr operations for duplication…
laqieer Feb 10, 2022
c0f114e
documentation: add another case to import fonts
laqieer Feb 10, 2022
d35a068
Merge branch 'GValiente:master' into master
laqieer Feb 10, 2022
5db65a0
fix: fonts overflow cell
laqieer Feb 10, 2022
7d70d87
examples: adjust y position of texts at the top/bottom a little to av…
laqieer Feb 10, 2022
481f208
examples: change current user fonts in example to open source font: s…
laqieer Feb 10, 2022
a53d241
Merge branch 'master' of github.com:GValiente/butano
laqieer Feb 10, 2022
032be6e
butano: update `butano_fonts_tool` for `bn::sprite_font`'s breaking c…
laqieer Feb 10, 2022
2dbb7b7
butano: change current common fonts for Chinese and Japanese to open …
laqieer Feb 10, 2022
25e6bb9
examples: test full-width punctuations in new Japanese font
laqieer Feb 10, 2022
bb54114
examples: test WRAM usage for new `bn::sprite_font`
laqieer Feb 10, 2022
5cec1d8
butano: common fonts for Korean including examples (open source font:…
laqieer Feb 10, 2022
2a539dc
butano: print utf-8 code in error info when UTF-8 character not found
laqieer Feb 10, 2022
74ca906
butano: common fonts for Traditional Chinese (Taiwan) including 4808 …
laqieer Feb 10, 2022
4ae8c4c
butano: common fonts for Traditional Chinese (Hong Kong) including 48…
laqieer Feb 10, 2022
46055a0
examples: common fonts contain 25000+ CJK characters
laqieer Feb 10, 2022
a047a50
butano: allow to use common fonts without BMFont
laqieer Feb 10, 2022
7a6ba1f
documentations: update guide to import fonts or use common fonts
laqieer Feb 10, 2022
971e744
butano: font licenses
laqieer Feb 11, 2022
661f715
documentations: name and license of common fonts
laqieer Feb 11, 2022
d0e90cc
documentations: README on how to use common fonts
laqieer Feb 11, 2022
63be46e
documentations: format README for common fonts
laqieer Feb 11, 2022
9713baa
Merge branch 'GValiente:master' into master
laqieer Feb 11, 2022
efaf48e
butano: remove compiler flag `-fconstexpr-ops-limit` in Makefile
laqieer Feb 11, 2022
cc70006
Seperate common fonts to its own repo: https://github.com/laqieer/gba…
laqieer Feb 11, 2022
722f96a
documentations: update guide and faq after common fonts separation ac…
laqieer Feb 11, 2022
0c6edde
fix: y position of characters considering baseline height offset
laqieer Feb 11, 2022
90c2722
butano: separate butano_fonts_tool to work as an external tool
laqieer Feb 11, 2022
721cdf4
template: remove bmfont
laqieer Feb 11, 2022
58040e5
butano: move build/fonts/ folder's creation job from Makefile to buta…
laqieer Feb 11, 2022
1f2ed8f
template: remove fonts folder
laqieer Feb 11, 2022
852c563
examples: separate example for bmfont from 'text' to a new one 'font'
laqieer Feb 11, 2022
82065c8
documentations: separated example font
laqieer Feb 11, 2022
701e846
butano: auto trim useless characters in fonts to save space
laqieer Feb 12, 2022
bc62c92
butano: accelerate fonts conversion with auto-trim feature
laqieer Feb 12, 2022
6e88fbb
butano: fix variable bmfont width calculation
laqieer Feb 12, 2022
cfeb99a
fix: variable width calculation and auto-trim acceleration (font)
laqieer Feb 12, 2022
2b692a1
fix: not read string value with inner spaces from .fnt file
laqieer Feb 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions butano/include/bn_documentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,12 @@
*
* Showcase of a 4096x4096 world map with a perspective effect.
*
* <tr colspan="3"><td> @ref font
*
* @image html examples_font.png
*
* Showcase of Butano BMFont support.
*
* </table>
*/

Expand Down Expand Up @@ -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 <a href="http://www.angelcode.com/products/bmfont/">Bitmap Font Generator</a>, <a href="https://snowb.org/">Bitmap Font Generator Online</a> or <a href="https://hahahoho.studio/">BMfont-web</a>.
*
* It is also a common font format used in generic game engines like <a href="https://docs.cocos.com/creator/manual/en/asset/font.html">Cocos</a>, <a href="https://assetstore.unity.com/packages/tools/gui/bitmap-font-importer-62128">Unity</a> and visual novel engines like <a href="https://www.renpy.org/doc/html/text.html?highlight=text#image-based-fonts">Ren'Py</a>.
*
* First of all, <a href="https://pillow.readthedocs.io/en/stable/installation.html">install Pillow</a>.
*
* @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
*/


Expand Down Expand Up @@ -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 <a href="https://github.com/laqieer/gba-free-fonts">here</a>.
*
* @subsection faq_tonc_general_notes Are there some more general notes on GBA programming out there?
*
Expand Down
2 changes: 1 addition & 1 deletion butano/include/bn_utf8_characters_map_ref.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
263 changes: 263 additions & 0 deletions butano/tools/butano_fonts_tool.py
Original file line number Diff line number Diff line change
@@ -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<const bn::utf8_character> ' + 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)
5 changes: 5 additions & 0 deletions credits/fonts.txt
Original file line number Diff line number Diff line change
@@ -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

Binary file added docs/examples_font.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading