[Font] Allow overriding advances, offsets and kerning in the ImageFont import settings. Fix bitmap font kerning override.

This commit is contained in:
bruvzg 2024-02-07 19:21:49 +02:00
parent f317cc713a
commit 42ec133dbe
No known key found for this signature in database
GPG key ID: 7960FCF39844EC38
3 changed files with 187 additions and 43 deletions

View file

@ -11,12 +11,16 @@
<link title="Bitmap fonts - Using fonts">$DOCS_URL/tutorials/ui/gui_using_fonts.html#bitmap-fonts</link>
</tutorials>
<members>
<member name="ascent" type="int" setter="" getter="" default="0">
Font ascent (number of pixels above the baseline). If set to [code]0[/code], half of the character height is used.
</member>
<member name="character_margin" type="Rect2i" setter="" getter="" default="Rect2i(0, 0, 0, 0)">
Margin applied around every imported glyph. If your font image contains guides (in the form of lines between glyphs) or if spacing between characters appears incorrect, try adjusting [member character_margin].
</member>
<member name="character_ranges" type="PackedStringArray" setter="" getter="" default="PackedStringArray()">
The character ranges to import from the font image. This is an array that maps each position on the image (in tile coordinates, not pixels). The font atlas is traversed from left to right and top to bottom. Characters can be specified with decimal numbers (127), hexadecimal numbers ([code]0x007f[/code]) or between single quotes ([code]'~'[/code]). Ranges can be specified with a hyphen between characters.
For instance, [code]0-127[/code] (or [code]0x0000-0x007f[/code]) denotes the full ASCII range. As another example, [code]' '-'~'[/code] is equivalent to [code]32-127[/code] and denotes the range of printable (visible) ASCII characters.
The character ranges to import from the font image. This is an array that maps each position on the image (in tile coordinates, not pixels). The font atlas is traversed from left to right and top to bottom. Characters can be specified with decimal numbers (127), hexadecimal numbers ([code]0x007f[/code], or [code]U+007f[/code]) or between single quotes ([code]'~'[/code]). Ranges can be specified with a hyphen between characters.
For example, [code]0-127[/code] represents the full ASCII range. It can also be written as [code]0x0000-0x007f[/code] (or [code]U+0000-U+007f[/code]). As another example, [code]' '-'~'[/code] is equivalent to [code]32-127[/code] and represents the range of printable (visible) ASCII characters.
For any range, the character advance and offset can be customized by appending three space-separated integer values (additional advance, x offset, y offset) to the end. For example [code]'a'-'b' 4 5 2[/code] sets the advance to [code]char_width + 4[/code] and offset to [code]Vector2(5, 2)[/code] for both `a` and `b` characters.
Make sure [member character_ranges] doesn't exceed the number of [member columns] * [member rows] defined. Otherwise, the font will fail to import.
</member>
<member name="columns" type="int" setter="" getter="" default="1">
@ -25,12 +29,19 @@
<member name="compress" type="bool" setter="" getter="" default="true">
If [code]true[/code], uses lossless compression for the resulting font.
</member>
<member name="descent" type="int" setter="" getter="" default="0">
Font descent (number of pixels below the baseline). If set to [code]0[/code], half of the character height is used.
</member>
<member name="fallbacks" type="Array" setter="" getter="" default="[]">
List of font fallbacks to use if a glyph isn't found in this bitmap font. Fonts at the beginning of the array are attempted first.
</member>
<member name="image_margin" type="Rect2i" setter="" getter="" default="Rect2i(0, 0, 0, 0)">
Margin to cut on the sides of the entire image. This can be used to cut parts of the image that contain attribution information or similar.
</member>
<member name="kerning_pairs" type="PackedStringArray" setter="" getter="" default="PackedStringArray()">
Kerning pairs for the font. Kerning pair adjust the spacing between two characters.
Each string consist of three space separated values: "from" string, "to" string and integer offset. Each combination form the two string for a kerning pair, e.g, [code]ab cd -3[/code] will create kerning pairs [code]ac[/code], [code]ad[/code], [code]bc[/code], and [code]bd[/code] with offset [code]-3[/code].
</member>
<member name="rows" type="int" setter="" getter="" default="1">
Number of rows in the font image. See also [member columns].
</member>

View file

@ -61,10 +61,13 @@ bool ResourceImporterImageFont::get_option_visibility(const String &p_path, cons
void ResourceImporterImageFont::get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset) const {
r_options->push_back(ImportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "character_ranges"), Vector<String>()));
r_options->push_back(ImportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "kerning_pairs"), Vector<String>()));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "columns"), 1));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "rows"), 1));
r_options->push_back(ImportOption(PropertyInfo(Variant::RECT2I, "image_margin"), Rect2i()));
r_options->push_back(ImportOption(PropertyInfo(Variant::RECT2I, "character_margin"), Rect2i()));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "ascent"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "descent"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::ARRAY, "fallbacks", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("Font")), Array()));
@ -72,30 +75,15 @@ void ResourceImporterImageFont::get_import_options(const String &p_path, List<Im
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "scaling_mode", PROPERTY_HINT_ENUM, "Disabled,Enabled (Integer),Enabled (Fractional)"), TextServer::FIXED_SIZE_SCALE_ENABLED));
}
bool ResourceImporterImageFont::_decode_range(const String &p_token, int32_t &r_pos) {
if (p_token.begins_with("U+") || p_token.begins_with("u+") || p_token.begins_with("0x")) {
// Unicode character hex index.
r_pos = p_token.substr(2).hex_to_int();
return true;
} else if (p_token.length() == 3 && p_token[0] == '\'' && p_token[2] == '\'') {
// Unicode character.
r_pos = p_token.unicode_at(1);
return true;
} else if (p_token.is_numeric()) {
// Unicode character decimal index.
r_pos = p_token.to_int();
return true;
} else {
return false;
}
}
Error ResourceImporterImageFont::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
print_verbose("Importing image font from: " + p_source_file);
int columns = p_options["columns"];
int rows = p_options["rows"];
int ascent = p_options["ascent"];
int descent = p_options["descent"];
Vector<String> ranges = p_options["character_ranges"];
Vector<String> kern = p_options["kerning_pairs"];
Array fallbacks = p_options["fallbacks"];
Rect2i img_margin = p_options["image_margin"];
Rect2i char_margin = p_options["character_margin"];
@ -130,39 +118,186 @@ Error ResourceImporterImageFont::import(const String &p_source_file, const Strin
font->set_texture_image(0, Vector2i(chr_height, 0), 0, img);
font->set_fixed_size_scale_mode(smode);
int pos = 0;
for (int i = 0; i < ranges.size(); i++) {
int32_t start, end;
Vector<String> tokens = ranges[i].split("-");
if (tokens.size() == 2) {
if (!_decode_range(tokens[0], start) || !_decode_range(tokens[1], end)) {
WARN_PRINT("Invalid range: \"" + ranges[i] + "\"");
int32_t pos = 0;
for (const String &range : ranges) {
int32_t start = -1;
int32_t end = -1;
int chr_adv = 0;
Vector2i chr_off;
{
enum RangeParseStep {
STEP_START_BEGIN,
STEP_START_READ_HEX,
STEP_START_READ_DEC,
STEP_END_BEGIN,
STEP_END_READ_HEX,
STEP_END_READ_DEC,
STEP_ADVANCE_BEGIN,
STEP_OFF_X_BEGIN,
STEP_OFF_Y_BEGIN,
STEP_FINISHED,
};
RangeParseStep step = STEP_START_BEGIN;
String token;
for (int c = 0; c < range.length(); c++) {
switch (step) {
case STEP_START_BEGIN:
case STEP_END_BEGIN: {
// Read range start/end first symbol.
if (range[c] == 'U' || range[c] == 'u') {
if ((c <= range.length() - 2) && range[c + 1] == '+') {
token = String();
if (step == STEP_START_BEGIN) {
step = STEP_START_READ_HEX;
} else {
step = STEP_END_READ_HEX;
}
c++; // Skip "+".
continue;
}
} else if (range[c] == '0') {
if ((c <= range.length() - 2) && range[c + 1] == 'x') {
token = String();
if (step == STEP_START_BEGIN) {
step = STEP_START_READ_HEX;
} else {
step = STEP_END_READ_HEX;
}
c++; // Skip "x".
continue;
}
} else if (range[c] == '\'' || range[c] == '\"') {
if ((c <= range.length() - 3) && (range[c + 2] == '\'' || range[c + 2] == '\"')) {
token = String();
if (step == STEP_START_BEGIN) {
start = range.unicode_at(c + 1);
step = STEP_END_BEGIN;
} else {
end = range.unicode_at(c + 1);
step = STEP_ADVANCE_BEGIN;
}
c = c + 2; // Skip the rest or token.
continue;
}
} else if (is_digit(range[c])) {
// Read decimal value, start.
c++;
token = String();
if (step == STEP_START_BEGIN) {
step = STEP_START_READ_DEC;
} else {
step = STEP_END_READ_DEC;
}
token += range[c];
continue;
}
[[fallthrough]];
}
case STEP_ADVANCE_BEGIN:
case STEP_OFF_X_BEGIN:
case STEP_OFF_Y_BEGIN: {
// Read advance and offset.
if (range[c] == ' ') {
int next = range.find(" ", c + 1);
if (next < c) {
next = range.length();
}
if (step == STEP_OFF_X_BEGIN) {
chr_off.x = range.substr(c + 1, next - (c + 1)).to_int();
step = STEP_OFF_Y_BEGIN;
} else if (step == STEP_OFF_Y_BEGIN) {
chr_off.y = range.substr(c + 1, next - (c + 1)).to_int();
step = STEP_FINISHED;
} else {
chr_adv = range.substr(c + 1, next - (c + 1)).to_int();
step = STEP_OFF_X_BEGIN;
}
c = next - 1;
continue;
}
} break;
case STEP_START_READ_HEX:
case STEP_END_READ_HEX: {
// Read hexadecimal value.
if (is_hex_digit(range[c])) {
token += range[c];
} else {
if (step == STEP_START_READ_HEX) {
start = token.hex_to_int();
step = STEP_END_BEGIN;
} else {
end = token.hex_to_int();
step = STEP_ADVANCE_BEGIN;
}
}
} break;
case STEP_START_READ_DEC:
case STEP_END_READ_DEC: {
// Read decimal value.
if (is_digit(range[c])) {
token += range[c];
} else {
if (step == STEP_START_READ_DEC) {
start = token.to_int();
step = STEP_END_BEGIN;
} else {
end = token.to_int();
step = STEP_ADVANCE_BEGIN;
}
}
} break;
default: {
WARN_PRINT(vformat("Invalid character \"%d\" in the range: \"%s\"", c, range));
} break;
}
}
if (end == -1) {
end = start;
}
if (start == -1) {
WARN_PRINT(vformat("Invalid range: \"%s\"", range));
continue;
}
} else if (tokens.size() == 1) {
if (!_decode_range(tokens[0], start)) {
WARN_PRINT("Invalid range: \"" + ranges[i] + "\"");
continue;
}
end = start;
} else {
WARN_PRINT("Invalid range: \"" + ranges[i] + "\"");
continue;
}
for (int32_t idx = start; idx <= end; idx++) {
for (int32_t idx = MIN(start, end); idx <= MAX(start, end); idx++) {
ERR_FAIL_COND_V_MSG(pos >= count, ERR_CANT_CREATE, "Too many characters in range, should be " + itos(columns * rows));
int x = pos % columns;
int y = pos / columns;
font->set_glyph_advance(0, chr_height, idx, Vector2(chr_width, 0));
font->set_glyph_offset(0, Vector2i(chr_height, 0), idx, Vector2i(0, -0.5 * chr_height));
font->set_glyph_advance(0, chr_height, idx, Vector2(chr_width + chr_adv, 0));
font->set_glyph_offset(0, Vector2i(chr_height, 0), idx, Vector2i(0, -0.5 * chr_height) + chr_off);
font->set_glyph_size(0, Vector2i(chr_height, 0), idx, Vector2(chr_width, chr_height));
font->set_glyph_uv_rect(0, Vector2i(chr_height, 0), idx, Rect2(img_margin.position.x + chr_cell_width * x + char_margin.position.x, img_margin.position.y + chr_cell_height * y + char_margin.position.y, chr_width, chr_height));
font->set_glyph_texture_idx(0, Vector2i(chr_height, 0), idx, 0);
pos++;
}
}
font->set_cache_ascent(0, chr_height, 0.5 * chr_height);
font->set_cache_descent(0, chr_height, 0.5 * chr_height);
for (const String &kp : kern) {
const Vector<String> &kp_tokens = kp.split(" ");
if (kp_tokens.size() != 3) {
WARN_PRINT(vformat("Invalid kerning pairs string: \"%s\"", kp));
continue;
}
int offset = kp_tokens[2].to_int();
for (int a = 0; a < kp_tokens[0].length(); a++) {
for (int b = 0; b < kp_tokens[1].length(); b++) {
font->set_kerning(0, chr_height, Vector2i(kp_tokens[0].unicode_at(a), kp_tokens[1].unicode_at(b)), Vector2(offset, 0));
}
}
}
if (ascent > 0) {
font->set_cache_ascent(0, chr_height, ascent);
} else {
font->set_cache_ascent(0, chr_height, 0.5 * chr_height);
}
if (descent > 0) {
font->set_cache_descent(0, chr_height, descent);
} else {
font->set_cache_descent(0, chr_height, 0.5 * chr_height);
}
int flg = 0;
if ((bool)p_options["compress"]) {

View file

@ -39,8 +39,6 @@ class ResourceImporterImageFont : public ResourceImporter {
GDCLASS(ResourceImporterImageFont, ResourceImporter);
public:
static bool _decode_range(const String &p_token, int32_t &r_pos);
virtual String get_importer_name() const override;
virtual String get_visible_name() const override;
virtual void get_recognized_extensions(List<String> *p_extensions) const override;