/* * 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 #include #include #include #include #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 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); }