Files
Goober5000andClaude Opus 4.8 f3817c80fa Scale font line and tab advance by the font scale factor
Multi-line and tabbed strings drawn with a scaleMultiplier != 1 overlapped
or misaligned because the font code advanced newlines and tabs by the
unscaled font height/tab width -- most visible in FRED's viewport labels
now that they honor an adjustable font scale.

Scale the newline and tab advance in both the rendering and measurement
paths of each backend: the NVG path (gr_string and NVGFont::getStringSize)
and the VFNT path (gr_string_old and VFNTFont::getStringSize), which also
gains tab handling.  The tab width is hoisted onto the shared FSFont base
so both backends share it.

While here, pass the FSFont wrapper to gr_string_old instead of a bag of
scalars, make its font pointer const, and consolidate the
get_char_width_old externs into a single declaration in font_internal.h.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-09-03 01:55:03 -04:00

897 lines
21 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include <cstdio>
#include <cstdarg>
#include <string>
#include <sstream>
#include <climits>
#include "graphics/software/font_internal.h"
#include "graphics/software/FontManager.h"
#include "graphics/software/FSFont.h"
#include "graphics/software/VFNTFont.h"
#include "graphics/software/NVGFont.h"
#include "graphics/2d.h"
#include "mod_table/mod_table.h"
#include "def_files/def_files.h"
#include "bmpman/bmpman.h"
#include "cfile/cfile.h"
#include "localization/localize.h"
#include "parse/parselo.h"
#include "tracing/Monitor.h"
namespace
{
namespace fo = font;
using namespace font;
bool font_initialized = false;
constexpr ubyte DEFAULT_SPECIAL_CHAR_INDEX = 0;
// max allowed special char index; i.e. 7 special chars in retail fonts 1 & 3
const int MAX_SPECIAL_CHAR_IDX = UCHAR_MAX - 6;
bool parse_type(FontType &type, SCP_string &fileName)
{
int num = optional_string_either("$TrueType:", "$Font:");
if (num == 0)
{
type = NVG_FONT;
}
else if (num == 1)
{
type = VFNT_FONT;
}
else
{
type = UNKNOWN_FONT;
}
if (type != UNKNOWN_FONT)
{
stuff_string(fileName, F_NAME);
return true;
}
else
{
return false;
}
}
bool parse_nvg_font(const SCP_string& fontFilename)
{
float size = 8.0f;
SCP_string fontStr;
bool hasName = false;
if (optional_string("+Name:"))
{
stuff_string(fontStr, F_NAME);
hasName = true;
}
if (optional_string("+Size:"))
{
stuff_float(&size);
if (size <= 0.0f)
{
error_display(0, "+Size has to be bigger than 0 for font \"%s\".", fontFilename.c_str());
size = 8;
}
}
// This must happen before the font is loaded to set the size
bool autoSize = false;
if (optional_string("+Auto Size:")) {
stuff_boolean(&autoSize);
if (autoSize && !Fred_running) {
size = calculate_auto_font_size(size);
}
}
// Build name from existing values if no name is specified
if (!hasName)
{
SCP_stringstream ss;
ss << fontFilename << "-";
ss << size;
fontStr = ss.str();
}
if (FontManager::getFont(fontStr) != NULL)
{
if (hasName)
{
error_display(0, "Font with name \"%s\" is already present! Font names have to be unique!", fontStr.c_str());
return false;
}
else
{
error_display(0, "Found font with same default name (\"%s\"). This is most likely a duplicate.", fontStr.c_str());
return false;
}
}
auto nvgPair = FontManager::loadNVGFont(fontFilename, size);
auto nvgFont = nvgPair.first;
if (nvgFont == NULL)
{
error_display(0, "Couldn't load font \"%s\".", fontFilename.c_str());
return false;
}
// Now we can set the auto size behavior which is used for special character rendering
nvgFont->setAutoScaleBehavior(autoSize);
if (optional_string("+Can Scale:")) {
bool temp;
stuff_boolean(&temp);
nvgFont->setScaleBehavior(temp);
}
if (optional_string("+Top offset:"))
{
float temp;
stuff_float(&temp);
nvgFont->setTopOffset(temp);
}
if (optional_string("+Bottom offset:"))
{
float temp;
stuff_float(&temp);
nvgFont->setBottomOffset(temp);
}
if (optional_string("+Tab width:"))
{
float temp;
stuff_float(&temp);
if (temp < 0.0f)
{
error_display(0, "Invalid tab width %f! Has to be greater than or equal to zero.", temp);
}
else
{
nvgFont->setTabWidth(temp);
}
}
if (optional_string("+Letter spacing:"))
{
float temp;
stuff_float(&temp);
if (temp < 0.0f)
{
error_display(0, "Invalid letter spacing %f! Has to be greater than or equal to zero.", temp);
}
else
{
nvgFont->setLetterSpacing(temp);
}
}
if (!Unicode_text_mode) {
int special_char_index = DEFAULT_SPECIAL_CHAR_INDEX;
// Special characters only exist in non-Unicode mode
if (optional_string("+Special Character Font:"))
{
SCP_string fontName;
stuff_string(fontName, F_NAME);
fo::font* fontData = FontManager::loadFontOld(fontName.c_str());
if (fontData == NULL)
{
error_display(0, "Failed to load font \"%s\" for special characters of font \"%s\"!", fontName.c_str(), fontFilename.c_str());
}
else
{
nvgFont->setSpecialCharacterFont(fontData);
}
if (optional_string("+Special Character Index:")) {
stuff_int(&special_char_index);
if (special_char_index < 0 || special_char_index >= MAX_SPECIAL_CHAR_IDX) {
Warning(LOCATION, "Special character index (%d) for font (%s) is invalid, must be 0 - %u. Defaulting to 0.",
special_char_index, fontName.c_str(), MAX_SPECIAL_CHAR_IDX);
special_char_index = DEFAULT_SPECIAL_CHAR_INDEX;
}
}
else {
int old_index = FontManager::getFontIndexByFilename(fontName);
if (old_index >= 0) {
special_char_index = Lcl_languages[0].special_char_indexes[old_index];
} else {
Warning(LOCATION,
"A \"+Special Character Index:\" was not specified after the \"+Special Character Font:\" entry for the %s font. And a previous entry with a \"+Special Character Index:\" was not found."
"\n\n This entry must be specified directly or indirectly to avoid this warning. Defaulting to 0.",
fontStr.c_str());
}
}
}
else
{
fo::font* fontData = FontManager::loadFontOld("font01.vf");
if (fontData == nullptr) {
error_display(0,
"Failed to load default font \"%s\" for special characters of font \"%s\"! "
"This font is required for rendering special characters, if another font is not defined, and will cause an error later.",
"font01.vf",
fontFilename.c_str());
} else {
nvgFont->setSpecialCharacterFont(fontData);
}
}
int font_id = nvgPair.second;
// add the index specified to all languages
for (auto & Lcl_language : Lcl_languages) {
// if there wasn't already something specified for this font.
if (font_id >= (int)Lcl_language.special_char_indexes.size()) {
// if there were absolutely no special characters defined, put the default value as the language default.
if (Lcl_language.special_char_indexes.empty() == 0) {
Lcl_language.special_char_indexes.push_back(DEFAULT_SPECIAL_CHAR_INDEX);
}
// if somehow a lot of indexes were missing, add the language default to fill in the gaps until ...
while (font_id > (int)Lcl_language.special_char_indexes.size()) {
Lcl_language.special_char_indexes.push_back(Lcl_language.special_char_indexes[0]);
}
// we're at just the right place in the vector to add the index we just read from the table.
Lcl_language.special_char_indexes.push_back((ubyte)special_char_index);
} // replace if there was something already specified, except if nothing was specified in the table
else if (special_char_index != DEFAULT_SPECIAL_CHAR_INDEX) {
Lcl_language.special_char_indexes[font_id] = (ubyte)special_char_index;
}
}
}
nvgFont->setName(fontStr);
nvgFont->setFilename(fontFilename);
// Make sure that the height is not invalid
nvgFont->computeFontMetrics();
return true;
}
bool parse_vfnt_font(const SCP_string& fontFilename)
{
auto vfntPair = FontManager::loadVFNTFont(fontFilename);
auto font = vfntPair.first;
if (font == NULL)
{
error_display(0, "Couldn't load font\"%s\".", fontFilename.c_str());
return false;
}
SCP_string fontName;
if (optional_string("+Name:"))
{
stuff_string(fontName, F_NAME);
}
else
{
fontName.assign(fontFilename);
}
font->setName(fontName);
font->setFilename(fontFilename);
font->setFamilyName("Volition Font");
int font_id = vfntPair.second;
int user_defined_default_special_char_index = (int)DEFAULT_SPECIAL_CHAR_INDEX;
// 'default' special char index for all languages using this font
if (optional_string("+Default Special Character Index:")) {
stuff_int(&user_defined_default_special_char_index);
if (user_defined_default_special_char_index < 0 || user_defined_default_special_char_index >= MAX_SPECIAL_CHAR_IDX) {
Warning(LOCATION, "Default special character index (%d) for font (%s), must be 0 - %u. Defaulting to 0.", user_defined_default_special_char_index,
fontName.c_str(), MAX_SPECIAL_CHAR_IDX);
}
}
// add the index specified to all the languages
for (auto & Lcl_language : Lcl_languages) {
// if there wasn't already something specified for this font.
if (font_id >= (int)Lcl_language.special_char_indexes.size()) {
// if the default for the language was not already specified, set that default to the general default
if (Lcl_language.special_char_indexes.empty()) {
Lcl_language.special_char_indexes.push_back(DEFAULT_SPECIAL_CHAR_INDEX);
}
// if a lot of indexes were missing, add some defaults to fill in the gaps until ...
while (font_id > (int)Lcl_language.special_char_indexes.size()) {
// take into account retail special character settings for font index 2 (size is 1 indexed)
if (Lcl_language.special_char_indexes.size() == FONT3 - 1) {
Lcl_language.special_char_indexes.push_back(176);
}
else {
Lcl_language.special_char_indexes.push_back(Lcl_language.special_char_indexes[0]);
}
}
// we're at just the right place in the vector to add the index we just read from the table.
Lcl_language.special_char_indexes.push_back((ubyte)DEFAULT_SPECIAL_CHAR_INDEX);
} // if there was something already specified that's being replaced.
else if (user_defined_default_special_char_index != DEFAULT_SPECIAL_CHAR_INDEX) {
Lcl_language.special_char_indexes[font_id] = (ubyte)user_defined_default_special_char_index;
}
}
while (optional_string("+Language:")) {
char lang_name[LCL_LANG_NAME_LEN + 1];
int special_char_index, lang_idx = -1;
stuff_string(lang_name, F_NAME, LCL_LANG_NAME_LEN + 1);
// find language and set the index, or if not found move to the next one
lang_idx = lcl_find_lang_index_by_name(lang_name);
if (lang_idx == -1) {
Warning(LOCATION, "Ignoring invalid language (%s) specified by font (%s); not built-in or in strings.tbl",
lang_name, fontName.c_str());
skip_to_start_of_string_either("+Language:", "$Font:", "#End");
continue;
}
if (optional_string("+Special Character Index:")) {
stuff_int(&special_char_index);
if (special_char_index < 0 || special_char_index >= MAX_SPECIAL_CHAR_IDX) {
Error(LOCATION, "Special character index (%d) for font (%s), language (%s) is invalid, must be 0 - %u",
special_char_index, fontName.c_str(), lang_name, MAX_SPECIAL_CHAR_IDX);
}
Lcl_languages[lang_idx].special_char_indexes[font_id] = (ubyte)special_char_index;
}
}
if (optional_string_either("+Auto Size", "+Auto Size:") >= 0) {
bool temp;
stuff_boolean(&temp);
font->setAutoScaleBehavior(temp);
}
if (optional_string("+Can Scale:")) {
bool temp;
stuff_boolean(&temp);
font->setScaleBehavior(temp);
}
if (optional_string("+Top offset:"))
{
float temp;
stuff_float(&temp);
font->setTopOffset(temp);
}
if (optional_string("+Bottom offset:"))
{
float temp;
stuff_float(&temp);
font->setBottomOffset(temp);
}
if (optional_string("+Tab width:"))
{
float temp;
stuff_float(&temp);
if (temp < 0.0f)
{
error_display(0, "Invalid tab width %f! Has to be greater than or equal to zero.", temp);
}
else
{
font->setTabWidth(temp);
}
}
// Make sure that the height is not invalid
font->computeFontMetrics();
return true;
}
void font_parse_setup(const char *fileName)
{
bool noTable = false;
if (!strcmp(fileName, "fonts.tbl"))
{
if (!cf_exists_full(fileName, CF_TYPE_TABLES))
{
noTable = true;
}
}
if (noTable)
{
read_file_text_from_default(defaults_get_file("fonts.tbl"));
}
else
{
read_file_text(fileName, CF_TYPE_TABLES);
}
reset_parse();
// start parsing
required_string("#Fonts");
}
void parse_font_tbl(const char *fileName)
{
try
{
font_parse_setup(fileName);
FontType type;
SCP_string fontName;
SCP_vector<SCP_string> skipped_font_names;
while (parse_type(type, fontName))
{
bool parsed = false;
switch (type)
{
case VFNT_FONT:
if (Unicode_text_mode) {
skipped_font_names.push_back(fontName);
} else {
parsed = parse_vfnt_font(fontName);
}
break;
case NVG_FONT:
parsed = parse_nvg_font(fontName);
break;
default:
error_display(0, "Unknown font type %d! Get a coder!", (int)type);
break;
}
if (!parsed)
skip_to_start_of_string_one_of({ "$TrueType:", "$Font:", "#End" });
}
// check if we skipped any fonts
if (!skipped_font_names.empty()) {
Warning(LOCATION, "One or more bitmap fonts were skipped because they are not supported in Unicode text mode. The list of fonts is in the debug log.");
for (const auto& skippedFont : skipped_font_names) {
mprintf(("Warning: Skipped bitmap font: %s\n", skippedFont.c_str()));
}
}
// done parsing
required_string("#End");
}
catch (const parse::ParseException& e)
{
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", fileName, e.what()));
}
}
void parse_fonts_tbl()
{
//Parse main TBL first
parse_font_tbl("fonts.tbl");
//Then other ones
parse_modular_table("*-fnt.tbm", parse_font_tbl);
// double check
if (FontManager::numberOfFonts() < 3) {
Error(LOCATION, "At least three fonts have to be loaded but only %d valid entries were found!", FontManager::numberOfFonts());
}
}
}
namespace font
{
void init()
{
if (font_initialized) {
// Already initialized
return;
}
FontManager::init();
parse_fonts_tbl();
set_font(0);
font_initialized = true;
}
void checkFontOptions() {
if (!FontManager::hasScalingFonts()) {
removeFontMultiplierOption();
}
}
void close()
{
if (!font_initialized) {
return;
}
FontManager::close();
font_initialized = false;
}
int force_fit_string(char *str, size_t max_str_len, int max_width, float scale)
{
if (max_width <= 0 || max_str_len == 0) {
*str = 0;
return 0;
}
size_t len = strlen(str);
if (len > max_str_len) {
len = max_str_len;
str[len] = 0;
}
int w;
gr_get_string_size(&w, nullptr, str, scale);
if (w > max_width) {
constexpr char ellipsis_char = '.';
constexpr size_t ellipsis_len = 3;
// make sure an ellipsis will fit
if (len < ellipsis_len) {
*str = 0;
return 0;
}
// replace the last few chars with an ellipsis
for (size_t i = 0; i < ellipsis_len; ++i) {
--len;
str[len] = ellipsis_char;
}
// measure with ellipsis before shrinking further (the ellipsis characters could be narrower than the characters they replaced)
gr_get_string_size(&w, nullptr, str, scale, len + ellipsis_len);
// move the ellipsis back until the whole string fits
while (len > 0 && w > max_width) {
--len;
str[len] = ellipsis_char;
gr_get_string_size(&w, nullptr, str, scale, len + ellipsis_len);
}
str[len + ellipsis_len] = 0;
}
return w;
}
void stuff_first(SCP_string &firstFont)
{
try
{
font_parse_setup("fonts.tbl");
FontType type;
parse_type(type, firstFont);
}
catch (const parse::ParseException& e)
{
Error(LOCATION, "Failed to setup font parsing. This may be caused by an empty fonts.tbl file.\nError message: %s", e.what());
firstFont = "";
}
}
int parse_font()
{
int font_idx;
SCP_string input;
stuff_string(input, F_NAME);
SCP_stringstream ss(input);
int fontNum;
ss >> fontNum;
if (ss.fail())
{
fontNum = FontManager::getFontIndex(input);
if (fontNum < 0)
{
error_display(0, "Invalid font name \"%s\"!", input.c_str());
font_idx = -1;
}
else
{
font_idx = fontNum;
}
}
else
{
if (fontNum < 0 || fontNum >= FontManager::numberOfFonts())
{
error_display(0, "Invalid font number %d! Must be greater than or equal to zero and smaller than %d.", fontNum, FontManager::numberOfFonts());
font_idx = -1;
}
else
{
font_idx = fontNum;
}
}
return font_idx;
}
FSFont *get_current_font()
{
return FontManager::getCurrentFont();
}
FSFont *get_font(const SCP_string& name)
{
return FontManager::getFont(name);
}
FSFont *get_font_by_filename(const SCP_string& filename)
{
return FontManager::getFontByFilename(filename);
}
/**
* @brief Gets the width of an character.
*
* Returns the width of the specified charachter also taking account of kerning.
*
* @param fnt The font data
* @param c1 The character that should be checked.
* @param c2 The character which follows this character. Used to compute the kerning
* @param [out] width If non-null, the width.
* @param [out] spaceing If non-null, the spaceing.
*
* @return The character width.
*/
int get_char_width_old(const fo::font* fnt, ubyte c1, ubyte c2, int *width, int* spacing)
{
int i, letter;
letter = c1 - fnt->first_ascii;
//not in font, draw as space
if (letter < 0 || letter >= fnt->num_chars)
{
*width = 0;
*spacing = fnt->w;
return -1;
}
*width = fnt->char_data[letter].byte_width;
*spacing = fnt->char_data[letter].spacing;
i = fnt->char_data[letter].kerning_entry;
if (i > -1)
{
if (!(c2 == 0 || c2 == '\n'))
{
int letter2;
letter2 = c2 - fnt->first_ascii;
if ((letter2 >= 0) && (letter2 < fnt->num_chars) && (i < fnt->num_kern_pairs))
{
font_kernpair *k = &fnt->kern_data[i];
while ((i < fnt->num_kern_pairs) && (k->c1 == (char)letter) && (k->c2 < (char)letter2))
{
i++;
k++;
}
if ((i < fnt->num_kern_pairs) && (k->c2 == (char)letter2))
{
*spacing += k->offset;
}
}
}
}
return letter;
}
}
int gr_get_font_height()
{
if (FontManager::isReady())
{
return fl2i(FontManager::getCurrentFont()->getHeight());
}
else
{
return 16;
}
}
int gr_get_dynamic_font_lines(int number_default_lines) {
// the default font height (font01) is 9, thus the *10 --wookieejedi
return fl2i((number_default_lines * 10) / (gr_get_font_height() + 1));
}
void gr_get_string_size(int* w1, int* h1, const char* text, float scaleMultiplier, size_t len)
{
if (!FontManager::isReady())
{
if (w1)
*w1 = 16;
if (h1)
*h1 = 16;
return;
}
float w = 0.0f;
float h = 0.0f;
FontManager::getCurrentFont()->getStringSize(text, len, -1, &w, &h, scaleMultiplier);
if (w1)
{
*w1 = fl2i(ceil(w));
}
if (h1)
{
*h1 = fl2i(ceil(h));
}
}
MONITOR(FontChars)
#ifdef _WIN32
void gr_string_win(int x, int y, char *s)
{
using namespace font;
int old_bitmap = gr_screen.current_bitmap;
set_font(FONT1);
gr_string(x, y, s);
gr_screen.current_bitmap = old_bitmap;
}
#endif // ifdef _WIN32
static char grx_printf_text[2048];
void gr_printf_args(int resize_mode, int x, int y, size_t len, SCP_FORMAT_STRING const char *format, va_list args)
{
if (!FontManager::isReady())
return;
len = std::min(len, sizeof(grx_printf_text) - 1);
vsnprintf(grx_printf_text, len+1, format, args);
grx_printf_text[len] = '\0';
gr_string(x, y, grx_printf_text, resize_mode, 1.0f, len);
}
void gr_printf(int x, int y, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_FULL, x, y, std::string::npos, format, args);
va_end(args);
}
void gr_printf(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_FULL, x, y, len, format, args);
va_end(args);
}
void gr_printf_menu(int x, int y, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_MENU, x, y, std::string::npos, format, args);
va_end(args);
}
void gr_printf_menu(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_MENU, x, y, len, format, args);
va_end(args);
}
void gr_printf_menu_zoomed(int x, int y, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_MENU_ZOOMED, x, y, std::string::npos, format, args);
va_end(args);
}
void gr_printf_menu_zoomed(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_MENU_ZOOMED, x, y, len, format, args);
va_end(args);
}
void gr_printf_no_resize(int x, int y, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_NONE, x, y, std::string::npos, format, args);
va_end(args);
}
void gr_printf_no_resize(int x, int y, size_t len, SCP_FORMAT_STRING const char *format, ...)
{
va_list args;
va_start(args, format);
gr_printf_args(GR_RESIZE_NONE, x, y, len, format, args);
va_end(args);
}