// // SuperTuxKart - a fun racing game with go-kart // Copyright (C) 2016 SuperTuxKart-Team // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 3 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "font/font_with_face.hpp" #include "config/user_config.hpp" #include "font/face_ttf.hpp" #include "font/font_manager.hpp" #include "font/font_settings.hpp" #include "graphics/2dutils.hpp" #include "graphics/central_settings.hpp" #include "graphics/irr_driver.hpp" #include "graphics/stk_texture.hpp" #include "graphics/stk_tex_manager.hpp" #include "guiengine/engine.hpp" #include "guiengine/skin.hpp" #include "modes/profile_world.hpp" #include "utils/string_utils.hpp" #include // ---------------------------------------------------------------------------- /** Constructor. It will initialize the \ref m_spritebank and TTF files to use. * \param name The name of face, used by irrlicht to distinguish spritebank. * \param ttf \ref FaceTTF for this face to use. */ FontWithFace::FontWithFace(const std::string& name, FaceTTF* ttf) { m_spritebank = irr_driver->getGUI()->addEmptySpriteBank(name.c_str()); assert(m_spritebank != NULL); m_spritebank->grab(); m_fallback_font = NULL; m_fallback_font_scale = 1.0f; m_glyph_max_height = 0; m_face_ttf = ttf; } // FontWithFace // ---------------------------------------------------------------------------- /** Destructor. Clears the glyph page and sprite bank. */ FontWithFace::~FontWithFace() { for (unsigned int i = 0; i < m_spritebank->getTextureCount(); i++) { STKTexManager::getInstance()->removeTexture( static_cast(m_spritebank->getTexture(i))); } m_spritebank->drop(); m_spritebank = NULL; // To be deleted by font_manager m_face_ttf = NULL; } // ~FontWithFace // ---------------------------------------------------------------------------- /** Initialize the font structure, but don't load glyph here. */ void FontWithFace::init() { setDPI(); #ifndef SERVER_ONLY // Get the max height for this face assert(m_face_ttf->getTotalFaces() > 0); FT_Face cur_face = m_face_ttf->getFace(0); font_manager->checkFTError(FT_Set_Pixel_Sizes(cur_face, 0, getDPI()), "setting DPI"); for (int i = 32; i < 128; i++) { // Test all basic latin characters const int idx = FT_Get_Char_Index(cur_face, (wchar_t)i); if (idx == 0) continue; font_manager->checkFTError(FT_Load_Glyph(cur_face, idx, FT_LOAD_DEFAULT), "setting max height"); const int height = cur_face->glyph->metrics.height / BEARING; if (height > m_glyph_max_height) m_glyph_max_height = height; } #endif reset(); } // init // ---------------------------------------------------------------------------- /** Clear all the loaded characters, sub-class can do pre-loading of characters * after this. */ void FontWithFace::reset() { m_new_char_holder.clear(); m_character_area_map.clear(); m_character_glyph_info_map.clear(); for (unsigned int i = 0; i < m_spritebank->getTextureCount(); i++) { STKTexManager::getInstance()->removeTexture( static_cast(m_spritebank->getTexture(i))); } m_spritebank->clear(); createNewGlyphPage(); } // reset // ---------------------------------------------------------------------------- /** Convert a character to a glyph index in one of the font in \ref m_face_ttf, * it will find the first TTF that supports this character, if the final * glyph_index is 0, this means such character is not supported by all TTFs in * \ref m_face_ttf. * \param c The character to be loaded. */ void FontWithFace::loadGlyphInfo(wchar_t c) { #ifndef SERVER_ONLY unsigned int font_number = 0; unsigned int glyph_index = 0; while (font_number < m_face_ttf->getTotalFaces()) { glyph_index = FT_Get_Char_Index(m_face_ttf->getFace(font_number), c); if (glyph_index > 0) break; font_number++; } m_character_glyph_info_map[c] = GlyphInfo(font_number, glyph_index); #endif } // loadGlyphInfo // ---------------------------------------------------------------------------- /** Create a new glyph page by filling it with transparent content. */ void FontWithFace::createNewGlyphPage() { #ifndef SERVER_ONLY uint8_t* data = new uint8_t[getGlyphPageSize() * getGlyphPageSize() * (CVS->isARBTextureSwizzleUsable() ? 1 : 4)](); #else uint8_t* data = NULL; #endif m_current_height = 0; m_used_width = 0; m_used_height = 0; STKTexture* stkt = new STKTexture(data, typeid(*this).name() + StringUtils::toString(m_spritebank->getTextureCount()), getGlyphPageSize(), #ifndef SERVER_ONLY CVS->isARBTextureSwizzleUsable() #else false #endif ); m_spritebank->addTexture(STKTexManager::getInstance()->addTexture(stkt)); } // createNewGlyphPage // ---------------------------------------------------------------------------- /** Render a glyph for a character into bitmap and save it into the glyph page. * \param c The character to be loaded. * \param c \ref GlyphInfo for the character. */ void FontWithFace::insertGlyph(wchar_t c, const GlyphInfo& gi) { #ifndef SERVER_ONLY assert(gi.glyph_index > 0); assert(gi.font_number < m_face_ttf->getTotalFaces()); FT_Face cur_face = m_face_ttf->getFace(gi.font_number); FT_GlyphSlot slot = cur_face->glyph; // Same face may be shared across the different FontWithFace, // so reset dpi each time font_manager->checkFTError(FT_Set_Pixel_Sizes(cur_face, 0, getDPI()), "setting DPI"); font_manager->checkFTError(FT_Load_Glyph(cur_face, gi.glyph_index, FT_LOAD_DEFAULT), "loading a glyph"); font_manager->checkFTError(shapeOutline(&(slot->outline)), "shaping outline"); font_manager->checkFTError(FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL), "rendering a glyph to bitmap"); // Convert to an anti-aliased bitmap FT_Bitmap* bits = &(slot->bitmap); core::dimension2du texture_size(bits->width + 1, bits->rows + 1); if ((m_used_width + texture_size.Width > getGlyphPageSize() && m_used_height + m_current_height + texture_size.Height > getGlyphPageSize()) || m_used_height + texture_size.Height > getGlyphPageSize()) { // Add a new glyph page if current one is full createNewGlyphPage(); } // Determine the linebreak location if (m_used_width + texture_size.Width > getGlyphPageSize()) { m_used_width = 0; m_used_height += m_current_height; m_current_height = 0; } const unsigned int cur_tex = m_spritebank->getTextureCount() -1; if (bits->buffer != NULL && !ProfileWorld::isNoGraphics()) { video::ITexture* tex = m_spritebank->getTexture(cur_tex); glBindTexture(GL_TEXTURE_2D, tex->getOpenGLTextureName()); assert(bits->pixel_mode == FT_PIXEL_MODE_GRAY); if (CVS->isARBTextureSwizzleUsable()) { glTexSubImage2D(GL_TEXTURE_2D, 0, m_used_width, m_used_height, bits->width, bits->rows, GL_RED, GL_UNSIGNED_BYTE, bits->buffer); } else { const unsigned int size = bits->width * bits->rows; uint8_t* image_data = new uint8_t[size * 4]; memset(image_data, 255, size * 4); for (unsigned int i = 0; i < size; i++) image_data[4 * i + 3] = bits->buffer[i]; glTexSubImage2D(GL_TEXTURE_2D, 0, m_used_width, m_used_height, bits->width, bits->rows, GL_RGBA, GL_UNSIGNED_BYTE, image_data); delete[] image_data; } if (tex->hasMipMaps()) glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); } // Store the rectangle of current glyph gui::SGUISpriteFrame f; gui::SGUISprite s; core::rect rectangle(m_used_width, m_used_height, m_used_width + bits->width, m_used_height + bits->rows); f.rectNumber = m_spritebank->getPositions().size(); f.textureNumber = cur_tex; // Add frame to sprite s.Frames.push_back(f); s.frameTime = 0; m_spritebank->getPositions().push_back(rectangle); m_spritebank->getSprites().push_back(s); // Save glyph metrics FontArea a; a.advance_x = cur_face->glyph->advance.x / BEARING; a.bearing_x = cur_face->glyph->metrics.horiBearingX / BEARING; const int cur_height = (cur_face->glyph->metrics.height / BEARING); const int cur_offset_y = cur_height - (cur_face->glyph->metrics.horiBearingY / BEARING); a.offset_y = m_glyph_max_height - cur_height + cur_offset_y; a.offset_y_bt = -cur_offset_y; a.spriteno = f.rectNumber; m_character_area_map[c] = a; // Store used area m_used_width += texture_size.Width; if (m_current_height < texture_size.Height) m_current_height = texture_size.Height; #endif } // insertGlyph // ---------------------------------------------------------------------------- /** Update the supported characters for this font if required. */ void FontWithFace::updateCharactersList() { if (m_fallback_font != NULL) m_fallback_font->updateCharactersList(); if (m_new_char_holder.empty()) return; for (const wchar_t& c : m_new_char_holder) { const GlyphInfo& gi = getGlyphInfo(c); insertGlyph(c, gi); } m_new_char_holder.clear(); } // updateCharactersList // ---------------------------------------------------------------------------- /** Write the current glyph page in png inside current running directory. * Mainly for debug use. * \param name The file name. */ void FontWithFace::dumpGlyphPage(const std::string& name) { #ifndef SERVER_ONLY for (unsigned int i = 0; i < m_spritebank->getTextureCount(); i++) { video::ITexture* tex = m_spritebank->getTexture(i); core::dimension2d size = tex->getSize(); video::ECOLOR_FORMAT col_format = tex->getColorFormat(); void* data = tex->lock(); video::IImage* image = irr_driver->getVideoDriver() ->createImageFromData(col_format, size, data, true/*ownForeignMemory*/); tex->unlock(); irr_driver->getVideoDriver()->writeImageToFile(image, std::string (name + "_" + StringUtils::toString(i) + ".png").c_str()); image->drop(); } #endif } // dumpGlyphPage // ---------------------------------------------------------------------------- /** Write the current glyph page in png inside current running directory. * Useful in gdb without parameter. */ void FontWithFace::dumpGlyphPage() { dumpGlyphPage("face"); } // dumpGlyphPage // ---------------------------------------------------------------------------- /** Set the face dpi which is resolution-dependent. * Normal text will range from 0.8, in 640x* resolutions (won't scale below * that) to 1.0, in 1024x* resolutions, and linearly up. * Bold text will range from 0.2, in 640x* resolutions (won't scale below * that) to 0.4, in 1024x* resolutions, and linearly up. */ void FontWithFace::setDPI() { const int screen_width = irr_driver->getFrameSize().Width; const int screen_height = irr_driver->getFrameSize().Height; if (UserConfigParams::m_hidpi_enabled) { float scale = screen_height / 480.0f; m_face_dpi = int(getScalingFactorTwo() * getScalingFactorOne() * scale); } else { float scale = std::max(0, screen_width - 640) / 564.0f; // attempt to compensate for small screens if (screen_width < 1200) scale = std::max(0, screen_width - 640) / 750.0f; if (screen_width < 900 || screen_height < 700) scale = std::min(scale, 0.05f); m_face_dpi = unsigned((getScalingFactorOne() + 0.2f * scale) * getScalingFactorTwo()); } } // setDPI // ---------------------------------------------------------------------------- /** Return the \ref FontArea about a character. * \param c The character to get. * \param[out] fallback_font Whether fallback font is used. */ const FontWithFace::FontArea& FontWithFace::getAreaFromCharacter(const wchar_t c, bool* fallback_font) const { std::map::const_iterator n = m_character_area_map.find(c); if (n != m_character_area_map.end()) { if (fallback_font != NULL) *fallback_font = false; return n->second; } else if (m_fallback_font != NULL && fallback_font != NULL) { *fallback_font = true; return m_fallback_font->getAreaFromCharacter(c, NULL); } // Not found, return the first font area, which is a white-space if (fallback_font != NULL) *fallback_font = false; return m_character_area_map.begin()->second; } // getAreaFromCharacter // ---------------------------------------------------------------------------- /** Get the dimension of text with support to different \ref FontSettings, * it will also do checking for missing characters in font and lazy load them. * \param text The text to be calculated. * \param font_settings \ref FontSettings to use. * \return The dimension of text */ core::dimension2d FontWithFace::getDimension(const wchar_t* text, FontSettings* font_settings) { #ifdef SERVER_ONLY return core::dimension2d(1, 1); #else const float scale = font_settings ? font_settings->getScale() : 1.0f; // Test if lazy load char is needed insertCharacters(text); updateCharactersList(); assert(m_character_area_map.size() > 0); core::dimension2d dim(0.0f, 0.0f); core::dimension2d this_line(0.0f, m_font_max_height * scale); for (const wchar_t* p = text; *p; ++p) { if (*p == L'\r' || // Windows breaks *p == L'\n' ) // Unix breaks { if (*p==L'\r' && p[1] == L'\n') // Windows breaks ++p; dim.Height += this_line.Height; if (dim.Width < this_line.Width) dim.Width = this_line.Width; this_line.Width = 0; continue; } bool fallback = false; const FontArea &area = getAreaFromCharacter(*p, &fallback); this_line.Width += getCharWidth(area, fallback, scale); } dim.Height += this_line.Height; if (dim.Width < this_line.Width) dim.Width = this_line.Width; core::dimension2d ret_dim(0, 0); ret_dim.Width = (u32)(dim.Width + 0.9f); // round up ret_dim.Height = (u32)(dim.Height + 0.9f); return ret_dim; #endif } // getDimension // ---------------------------------------------------------------------------- /** Calculate the index of the character in the text on a specific position. * \param text The text to be calculated. * \param pixel_x The specific position. * \param font_settings \ref FontSettings to use. * \return The index of the character, -1 means no character in such position. */ int FontWithFace::getCharacterFromPos(const wchar_t* text, int pixel_x, FontSettings* font_settings) const { const float scale = font_settings ? font_settings->getScale() : 1.0f; float x = 0; int idx = 0; while (text[idx]) { bool use_fallback_font = false; const FontArea &a = getAreaFromCharacter(text[idx], &use_fallback_font); x += getCharWidth(a, use_fallback_font, scale); if (x >= float(pixel_x)) return idx; ++idx; } return -1; } // getCharacterFromPos // ---------------------------------------------------------------------------- /** Render text and clip it to the specified rectangle if wanted, it will also * do checking for missing characters in font and lazy load them. * \param text The text to be rendering. * \param position The position to be rendering. * \param color The color used when rendering. * \param hcenter If rendered horizontally center. * \param vcenter If rendered vertically center. * \param clip If clipping is needed. * \param font_settings \ref FontSettings to use. * \param char_collector \ref FontCharCollector to render billboard text. */ void FontWithFace::render(const core::stringw& text, const core::rect& position, const video::SColor& color, bool hcenter, bool vcenter, const core::rect* clip, FontSettings* font_settings, FontCharCollector* char_collector) { #ifndef SERVER_ONLY const bool black_border = font_settings ? font_settings->useBlackBorder() : false; const bool rtl = font_settings ? font_settings->isRTL() : false; const float scale = font_settings ? font_settings->getScale() : 1.0f; const float shadow = font_settings ? font_settings->useShadow() : false; if (shadow) { assert(font_settings); // Avoid infinite recursion font_settings->setShadow(false); core::rect shadowpos = position; shadowpos.LowerRightCorner.X += 2; shadowpos.LowerRightCorner.Y += 2; render(text, shadowpos, font_settings->getShadowColor(), hcenter, vcenter, clip, font_settings); // Set back font_settings->setShadow(true); } core::position2d offset(float(position.UpperLeftCorner.X), float(position.UpperLeftCorner.Y)); core::dimension2d text_dimension; if (rtl || hcenter || vcenter || clip) { text_dimension = getDimension(text.c_str(), font_settings); if (hcenter) offset.X += (position.getWidth() - text_dimension.Width) / 2; else if (rtl) offset.X += (position.getWidth() - text_dimension.Width); if (vcenter) offset.Y += (position.getHeight() - text_dimension.Height) / 2; if (clip) { core::rect clippedRect(core::position2d (s32(offset.X), s32(offset.Y)), text_dimension); clippedRect.clipAgainst(*clip); if (!clippedRect.isValid()) return; } } // Collect character locations const unsigned int text_size = text.size(); core::array indices(text_size); core::array> offsets(text_size); std::vector fallback(text_size); // Test again if lazy load char is needed, // as some text isn't drawn with getDimension insertCharacters(text.c_str()); updateCharactersList(); for (u32 i = 0; i < text_size; i++) { wchar_t c = text[i]; if (c == L'\r' || // Windows breaks c == L'\n' ) // Unix breaks { if (c==L'\r' && text[i+1]==L'\n') c = text[++i]; offset.Y += m_font_max_height * scale; offset.X = float(position.UpperLeftCorner.X); if (hcenter) offset.X += (position.getWidth() - text_dimension.Width) >> 1; continue; } // if lineBreak bool use_fallback_font = false; const FontArea &area = getAreaFromCharacter(c, &use_fallback_font); fallback[i] = use_fallback_font; if (char_collector == NULL) { float glyph_offset_x = area.bearing_x * (fallback[i] ? m_fallback_font_scale : scale); float glyph_offset_y = area.offset_y * (fallback[i] ? m_fallback_font_scale : scale); offset.X += glyph_offset_x; offset.Y += glyph_offset_y; offsets.push_back(offset); offset.X -= glyph_offset_x; offset.Y -= glyph_offset_y; } else { // Billboard text specific, use offset_y_bt instead float glyph_offset_x = area.bearing_x * (fallback[i] ? m_fallback_font_scale : scale); float glyph_offset_y = area.offset_y_bt * (fallback[i] ? m_fallback_font_scale : scale); offset.X += glyph_offset_x; offset.Y += glyph_offset_y; offsets.push_back(offset); offset.X -= glyph_offset_x; offset.Y -= glyph_offset_y; } indices.push_back(area.spriteno); offset.X += getCharWidth(area, fallback[i], scale); } // for i < text_size // Do the actual rendering const int indice_amount = indices.size(); core::array& sprites = m_spritebank->getSprites(); core::array>& positions = m_spritebank->getPositions(); core::array* fallback_sprites; core::array>* fallback_positions; if (m_fallback_font != NULL) { fallback_sprites = &m_fallback_font->m_spritebank->getSprites(); fallback_positions = &m_fallback_font->m_spritebank->getPositions(); } else { fallback_sprites = NULL; fallback_positions = NULL; } const int sprite_amount = sprites.size(); if ((black_border || isBold()) && char_collector == NULL) { // Draw black border first, to make it behind the real character // which make script language display better video::SColor black(color.getAlpha(),0,0,0); for (int n = 0; n < indice_amount; n++) { const int sprite_id = indices[n]; if (!fallback[n] && (sprite_id < 0 || sprite_id >= sprite_amount)) continue; if (indices[n] == -1) continue; const int tex_id = (fallback[n] ? (*fallback_sprites)[sprite_id].Frames[0].textureNumber : sprites[sprite_id].Frames[0].textureNumber); core::rect source = (fallback[n] ? (*fallback_positions) [(*fallback_sprites)[sprite_id].Frames[0].rectNumber] : positions[sprites[sprite_id].Frames[0].rectNumber]); core::dimension2d size(0.0f, 0.0f); float cur_scale = (fallback[n] ? m_fallback_font_scale : scale); size.Width = source.getSize().Width * cur_scale; size.Height = source.getSize().Height * cur_scale; core::rect dest(offsets[n], size); video::ITexture* texture = (fallback[n] ? m_fallback_font->m_spritebank->getTexture(tex_id) : m_spritebank->getTexture(tex_id)); for (int x_delta = -2; x_delta <= 2; x_delta++) { for (int y_delta = -2; y_delta <= 2; y_delta++) { if (x_delta == 0 || y_delta == 0) continue; draw2DImage(texture, dest + core::position2d (float(x_delta), float(y_delta)), source, clip, black, true); } } } } for (int n = 0; n < indice_amount; n++) { const int sprite_id = indices[n]; if (!fallback[n] && (sprite_id < 0 || sprite_id >= sprite_amount)) continue; if (indices[n] == -1) continue; const int tex_id = (fallback[n] ? (*fallback_sprites)[sprite_id].Frames[0].textureNumber : sprites[sprite_id].Frames[0].textureNumber); core::rect source = (fallback[n] ? (*fallback_positions)[(*fallback_sprites)[sprite_id].Frames[0] .rectNumber] : positions[sprites[sprite_id].Frames[0].rectNumber]); core::dimension2d size(0.0f, 0.0f); float cur_scale = (fallback[n] ? m_fallback_font_scale : scale); size.Width = source.getSize().Width * cur_scale; size.Height = source.getSize().Height * cur_scale; core::rect dest(offsets[n], size); video::ITexture* texture = (fallback[n] ? m_fallback_font->m_spritebank->getTexture(tex_id) : m_spritebank->getTexture(tex_id)); if (fallback[n] || isBold()) { video::SColor top = GUIEngine::getSkin()->getColor("font::top"); video::SColor bottom = GUIEngine::getSkin() ->getColor("font::bottom"); top.setAlpha(color.getAlpha()); bottom.setAlpha(color.getAlpha()); std::array title_colors; if (CVS->isGLSL()) { title_colors = { { top, bottom, top, bottom } }; } else { title_colors = { { bottom, top, top, bottom } }; } if (char_collector != NULL) { char_collector->collectChar(texture, dest, source, title_colors.data()); } else { draw2DImage(texture, dest, source, clip, title_colors.data(), true); } } else { if (char_collector != NULL) { video::SColor colors[] = {color, color, color, color}; char_collector->collectChar(texture, dest, source, colors); } else { draw2DImage(texture, dest, source, clip, color, true); } } } #endif } // render