#include "stdafx.h" /* ----------------------------------------------------------------------------- Class: Font Desc: See header. Copyright (c) 2001-2003 by the person(s) listed below. All rights reserved. Chris Danford Glenn Maynard ----------------------------------------------------------------------------- */ #include "Font.h" #include "IniFile.h" #include "RageTextureManager.h" #include "RageUtil.h" #include "RageLog.h" #include "RageException.h" #include "FontManager.h" #include "GameState.h" #include "ThemeManager.h" #include "GameManager.h" #include "FontCharmaps.h" #include "FontCharAliases.h" const longchar Font::DEFAULT_GLYPH = 0xFFFFFF; FontPage::FontPage() { m_pTexture = NULL; } void FontPage::Load( const FontPageSettings &cfg ) { m_sTexturePath = cfg.TexturePath; // load texture m_sTexturePath.MakeLower(); RageTextureID ID(m_sTexturePath); ID.bStretch = true; m_pTexture = TEXTUREMAN->LoadTexture( ID ); ASSERT( m_pTexture != NULL ); // load character widths vector FrameWidths; int i; int default_width = m_pTexture->GetSourceFrameWidth(); if(cfg.DefaultWidth != -1) default_width = cfg.DefaultWidth; // Assume each character is the width of the frame by default. for( i=0; iGetNumFrames(); i++ ) { map::const_iterator it = cfg.GlyphWidths.find(i); if(it != cfg.GlyphWidths.end()) { FrameWidths.push_back(it->second); } else { FrameWidths.push_back(default_width); } } if( cfg.AddToAllWidths ) { for( int i=0; iGetNumFrames(); i++ ) FrameWidths[i] += cfg.AddToAllWidths; } if( cfg.ScaleAllWidthsBy != 1 ) { for( int i=0; iGetNumFrames(); i++ ) FrameWidths[i] = int(roundf( FrameWidths[i] * cfg.ScaleAllWidthsBy )); } m_iCharToGlyphNo = cfg.CharToGlyphNo; LineSpacing = cfg.LineSpacing; if(LineSpacing == -1) LineSpacing = m_pTexture->GetSourceFrameHeight(); /* All characters on a page have the same vertical spacing, baseline * and ascender. */ if(cfg.Baseline != -1 && cfg.Top == -1) RageException::Throw("Font %s has Baseline but no Top; must have both or neither", m_sTexturePath.GetString()); if(cfg.Baseline == -1 && cfg.Top != -1) RageException::Throw("Font %s has Baseline but no Top; must have both or neither", m_sTexturePath.GetString()); int baseline=0; if(cfg.Baseline == -1 && cfg.Top == -1) { /* We don't have a top and baseline. Assume we're centered in the * frame, and that LineSpacing is the total height. */ float center = m_pTexture->GetSourceFrameHeight()/2.0f; height = LineSpacing; baseline = int(center + LineSpacing/2); } else if(cfg.Baseline != -1 && cfg.Top != -1) { baseline = cfg.Baseline; height = baseline-cfg.Top; } /* Shift the character up so the top will be rendered at the baseline. */ vshift = (float) -baseline; SetTextureCoords(FrameWidths); SetExtraPixels(cfg.DrawExtraPixelsLeft, cfg.DrawExtraPixelsRight); LOG->Trace("Font %s: height %i, baseline %i ( == top %i)", m_sTexturePath.GetString(), height, baseline, baseline-height); } void FontPage::SetTextureCoords(const vector &widths) { for(int i = 0; i < m_pTexture->GetNumFrames(); ++i) { glyph g; g.fp = this; /* Make a copy of each texture rect, reducing each to the actual dimensions * of the character (most characters don't take a full block). */ g.rect = *m_pTexture->GetTextureCoordRect(i);; /* Set the width and height to the width and line spacing, respectively. */ g.width = float(widths[i]); g.height = float(m_pTexture->GetSourceFrameHeight()); /* By default, advance one pixel more than the width. (This could be * an option.) */ g.hadvance = int(g.width + 1); /* Do the same thing with X. Do this by changing the actual rendered * rect, instead of shifting it, so we don't render more than we need to. */ g.hshift = 0; { int iPixelsToChopOff = m_pTexture->GetSourceFrameWidth() - widths[i]; if((iPixelsToChopOff % 2) == 1) { /* We don't want to chop off an odd number of pixels, since that'll * put our texture coordinates between texels and make things blurrier. */ iPixelsToChopOff--; g.width++; } float fTexCoordsToChopOff = float(iPixelsToChopOff) / m_pTexture->GetSourceWidth(); g.rect.left += fTexCoordsToChopOff/2; g.rect.right -= fTexCoordsToChopOff/2; } g.Texture = m_pTexture; glyphs.push_back(g); } } void FontPage::SetExtraPixels(int DrawExtraPixelsLeft, int DrawExtraPixelsRight) { /* Hack: do one more than we were asked to; I think a lot of fonts are one * too low. */ DrawExtraPixelsRight++; DrawExtraPixelsLeft++; if((DrawExtraPixelsLeft % 2) == 1) DrawExtraPixelsLeft++; /* Adjust for DrawExtraPixelsLeft and DrawExtraPixelsRight. */ for(unsigned i = 0; i < glyphs.size(); ++i) { int iFrameWidth = m_pTexture->GetSourceFrameWidth(); float iCharWidth = glyphs[i].width; /* Extra pixels to draw to the left and right. We don't have to * worry about alignment here; CharWidth is always even (by * SetTextureCoords) and iFrameWidth are almost always even. */ float ExtraLeft = min( float(DrawExtraPixelsLeft), (iFrameWidth-iCharWidth)/2.0f ); float ExtraRight = min( float(DrawExtraPixelsRight), (iFrameWidth-iCharWidth)/2.0f ); /* Move left and expand right. */ glyphs[i].rect.left -= ExtraLeft / m_pTexture->GetTextureWidth(); glyphs[i].rect.right += ExtraRight / m_pTexture->GetTextureWidth(); glyphs[i].hshift -= ExtraLeft; glyphs[i].width += ExtraLeft + ExtraRight; } } FontPage::~FontPage() { if( m_pTexture != NULL ) TEXTUREMAN->UnloadTexture( m_pTexture ); } int Font::GetLineWidthInSourcePixels( const lstring &szLine ) const { int LineWidth = 0; for( unsigned i=0; iheight); return iLineHeight; } int Font::GetLineSpacingInSourcePixels( const lstring &szLine ) const { int iLineSpacing = 0; /* The spacing of a line is the spacing of its tallest used font page. XXX */ for( unsigned i=0; iLineSpacing); return iLineSpacing; } Font::Font() { //LOG->Trace( "Font::LoadFromFontName(%s)", sASCIITexturePath.GetString() ); m_iRefCount = 1; def = NULL; } Font::~Font() { Unload(); } void Font::Unload() { unsigned i; for(i = 0; i < pages.size(); ++i) delete pages[i]; pages.clear(); m_iCharToGlyph.clear(); def = NULL; /* Don't clear the refcount. We've unloaded, but that doesn't mean things * aren't still pointing to us. */ } void Font::Reload() { Unload(); ASSERT(!path.empty()); Load(path, Chars); } void Font::AddPage(FontPage *fp) { pages.push_back(fp); for(map::const_iterator it = fp->m_iCharToGlyphNo.begin(); it != fp->m_iCharToGlyphNo.end(); ++it) { m_iCharToGlyph[it->first] = &fp->glyphs[it->second]; } } void Font::MergeFont(Font &f) { for(map::iterator it = f.m_iCharToGlyph.begin(); it != f.m_iCharToGlyph.end(); ++it) { m_iCharToGlyph[it->first] = it->second; } pages.insert(pages.end(), f.pages.begin(), f.pages.end()); f.pages.clear(); } const glyph &Font::GetGlyph( longchar c ) const { ASSERT(c >= 0 && c <= 0xFFFFFF); /* See if there's a game-specific version of this character. */ int gc = FontManager::MakeGameGlyph(c, GAMESTATE->m_CurGame); map::const_iterator it = m_iCharToGlyph.find(gc); /* If there isn't, try the regular character. */ if(it == m_iCharToGlyph.end()) it = m_iCharToGlyph.find(c); /* If *that's* missing, use the default glyph. */ if(it == m_iCharToGlyph.end()) it = m_iCharToGlyph.find(DEFAULT_GLYPH); if(it == m_iCharToGlyph.end()) RageException::Throw( "The default glyph is missing from the font '%s'", path.GetString() ); return *it->second; } RageTexture *Font::GetGlyphTexture( longchar c ) { glyph &g = const_cast (GetGlyph(c)); return g.Texture; } void Font::CapsOnly() { /* For each uppercase character that we have a mapping for, add * a lowercase one. */ for(char c = 'A'; c <= 'Z'; ++c) { map::const_iterator it = m_iCharToGlyph.find(c); if(it == m_iCharToGlyph.end()) continue; m_iCharToGlyph[(char) tolower(c)] = it->second; } } void Font::SetDefaultGlyph(FontPage *fp) { ASSERT(fp); ASSERT(!fp->glyphs.empty()); def = fp; } /* "Normal.png", "Normal [foo].png", "Normal 16x16.png", and "Normal [foo] 16x16.png" * are all part of the FontName "Normal". "Normal2 16x16.png" is not. * * So, FileName must start with FontName, followed by either " [", " DIGIT" or * a period. * * This is to make sure we don't pull in textures from similarly-named fonts. */ bool Font::MatchesFont(CString FontName, CString FileName) { FontName.MakeLower(); FileName.MakeLower(); if(FileName.substr(0, FontName.size()) != FontName) return false; FileName = FileName.substr(FontName.size()); return regex("^( \\[|\\.| [0-9]+x[0-9]+)", FileName); } void Font::GetFontPaths(const CString &sFontOrTextureFilePath, CStringArray &TexturePaths, CString &IniPath) { CString sDrive, sDir, sFName, sExt; splitpath( false, sFontOrTextureFilePath, sDrive, sDir, sFName, sExt ); ASSERT(sExt.CompareNoCase("ini")); /* don't give us an INI! */ ASSERT(sExt.CompareNoCase("redir")); /* don't give us a redir! */ if(!sExt.empty()) { TexturePaths.push_back(sFontOrTextureFilePath); return; } /* If we have no extension, we need to search. */ GetDirListing( sFontOrTextureFilePath + "*.png", TexturePaths, false, true ); CStringArray IniPaths; GetDirListing( sFontOrTextureFilePath + "*.ini", IniPaths, false, true ); /* Filter out texture files that aren't actually for this font. */ unsigned i = 0; while(i < TexturePaths.size()) { if(!MatchesFont(sFontOrTextureFilePath, TexturePaths[i])) TexturePaths.erase(TexturePaths.begin()+i); else i++; } for(i = 0; i < IniPaths.size(); ++i) if(MatchesFont(sFontOrTextureFilePath, IniPaths[i])) break; if(i < IniPaths.size()) IniPath = IniPaths[i]; ASSERT(!TexturePaths.empty()); /* ThemeManager should have checked this */ } CString Font::GetPageNameFromFileName(const CString &fn) { unsigned begin = fn.find_first_of('['); if(begin == fn.npos) return "main"; unsigned end = fn.find_first_of(']', begin); if(end == fn.npos) return "main"; begin++; end--; if(end == begin) return "main"; return fn.substr(begin, end-begin+1); } void Font::LoadFontPageSettings(FontPageSettings &cfg, IniFile &ini, const CString &TexturePath, const CString &PageName, CString sChars) { cfg.TexturePath = TexturePath; /* If we have any characters to map, add them. */ for( unsigned n=0; nTrace("Loading font page '%s' settings from page name '%s'", TexturePath.GetString(), PageName.GetString()); ini.GetValueI( PageName, "DrawExtraPixelsLeft", cfg.DrawExtraPixelsLeft ); ini.GetValueI( PageName, "DrawExtraPixelsRight", cfg.DrawExtraPixelsRight ); ini.GetValueI( PageName, "AddToAllWidths", cfg.AddToAllWidths ); ini.GetValueF( PageName, "ScaleAllWidthsBy", cfg.ScaleAllWidthsBy ); ini.GetValueI( PageName, "LineSpacing", cfg.LineSpacing ); ini.GetValueI( PageName, "Top", cfg.Top ); ini.GetValueI( PageName, "Baseline", cfg.Baseline ); ini.GetValueI( PageName, "DefaultWidth", cfg.DefaultWidth ); /* Iterate over all keys. */ const IniFile::key *k = ini.GetKey(PageName); if(k == NULL) return; for(IniFile::key::const_iterator key = k->begin(); key != k->end(); ++key) { CString val = key->first; CString data = key->second; val.MakeUpper(); /* If val is an integer, it's a width, eg. "10=27". */ if(IsAnInt(val)) { cfg.GlyphWidths[atoi(val)] = atoi(data); continue; } /* "map XXXX=frame" maps a char to a frame. */ if(val.substr(0, 4) == "MAP ") { /* map CODEPOINT=frame. CODEPOINT can be * 1. U+hexval * 2. an alias ("kakumei1") * 3. a game type followed by a game alias, eg "pump menuleft" * * map 1=2 is the same as * range unicode #1-1=2 */ CString codepoint = val.substr(4); /* "XXXX" */ Game game = GAME_INVALID; if(codepoint.find_first_of(' ') != codepoint.npos) { /* There's a space; the first word should be a game type. Split it. */ unsigned pos = codepoint.find_first_of(' '); CString gamename = codepoint.substr(0, pos); codepoint = codepoint.substr(pos+1); game = GameManager::StringToGameType(gamename); if(game == GAME_INVALID) RageException::Throw( "Font definition '%s' uses unknown game type '%s'", ini.GetPath().GetString(), gamename.GetString() ); } longchar c=-1; if(codepoint.substr(0, 2) == "U+" && IsHexVal(codepoint.substr(2))) sscanf(codepoint.substr(2).c_str(), "%x", &c); else c = FontCharAliases::GetChar(codepoint); if(c == -1) RageException::Throw( "Font definition '%s' has an invalid value '%s'.", ini.GetPath().GetString(), val.GetString() ); if(game != GAME_INVALID) c = FontManager::MakeGameGlyph(c, game); cfg.CharToGlyphNo[c] = atoi(data); continue; } if(val.substr(0, 6) == "RANGE ") { /* range CODESET=first_frame or * range CODESET #start-end=first_frame * eg * range CP1252=0 (default for 256-frame fonts) * range ASCII=0 (default for 128-frame fonts) * * (Start and end are in hex.) * * Map two high-bit portions of ISO-8859- to one font: * range ISO-8859-2 #80-FF=0 * range ISO-8859-3 #80-FF=128 * * Map hiragana to 0-84: * range Unicode #3041-3094=0 */ vector matches; bool match = regex("^RANGE ([A-Z\\-]+)( ?#([0-9A-F]+)-([0-9A-F]+))?$" , val, matches); ASSERT(matches.size() == 4); /* 4 parens */ if(!match || matches[0].empty()) RageException::Throw("Font definition '%s' has an invalid range '%s': parse error", ini.GetPath().GetString(), val.GetString() ); /* We must have either 1 match (just the codeset) or 4 (the whole thing). */ int cnt = -1; int first = 0; if(!matches[2].empty()) { sscanf(matches[2].GetString(), "%x", &first); int last; sscanf(matches[3].GetString(), "%x", &last); if(last < first) RageException::Throw("Font definition '%s' has an invalid range '%s': %i < %i.", ini.GetPath().GetString(), val.GetString(), last < first ); cnt = last-first+1; } CString ret = cfg.MapRange(matches[0], first, atoi(data), cnt); if(!ret.empty()) RageException::Throw("Font definition '%s' has an invalid range '%s': %s.", ini.GetPath().GetString(), val.GetString(), ret.GetString() ); continue; } } /* If it's 128 or 256 frames, default to ASCII or CP1252, * respectively. If it's anything else, we don't know what it * is, so don't make any default mappings (the INI needs to do * it itself). */ if(cfg.CharToGlyphNo.empty() && NumFrames == 128) cfg.MapRange("ascii", 0, 0, -1); else if(cfg.CharToGlyphNo.empty() && NumFrames == 256) cfg.MapRange("cp1252", 0, 0, -1); } CString FontPageSettings::MapRange(CString Mapping, int map_offset, int glyphno, int cnt) { if(!Mapping.CompareNoCase("Unicode")) { /* Special case. */ if(cnt == -1) return "Can't map all of Unicode to one font page"; /* don't do that */ /* What's a practical limit? A 2048x2048 texture could contain 16x16 characters, * which is 16384 glyphs. (Use a grayscale map and that's only 4 megs.) Let's use * that as a cap. (We don't want to go crazy if someone says "range Unicode * #0-FFFFFFFF".) */ if(cnt > 16384) return ssprintf("Can't map %i glyphs to one font page", cnt); while(cnt) { CharToGlyphNo[map_offset] = glyphno; map_offset++; glyphno++; cnt--; } return ""; } const wchar_t *mapping = FontCharmaps::get_char_map(Mapping); if(mapping == NULL) return "Unknown mapping"; while(*mapping != 0 && map_offset) { mapping++; map_offset--; } if(map_offset) return "Map overflow"; /* there aren't enough characters in the map */ /* If cnt is -1, set it to the number of characters in the map. */ if(cnt == -1) for(cnt = 0; mapping[cnt] != 0; ++cnt) ; while(*mapping != 0) { if(*mapping != FontCharmaps::M_SKIP) CharToGlyphNo[*mapping] = glyphno; mapping++; glyphno++; cnt--; } if(cnt) return "Map overflow"; /* there aren't enough characters in the map */ return ""; } static CStringArray LoadStack; /* A font set is a set of files, eg: * * Normal 16x16.png * Normal [other] 16x16.png * Normal [more] 8x8.png * Normal 16x16.ini (the 16x16 here is optional) * * Only one texture is required; the INI is optional. [1] This is * designed to be backwards-compatible. * * sFontOrTextureFilePath can be a partial path, eg. * "Themes/default/Fonts/Normal" * or a complete path to a texture file (in which case no other * files will be searched for). * * The entire font can be redirected; that's handled in ThemeManager. * Individual font files can not be redirected. * * TODO: * [main] * import=FontName,FontName2 (load other fonts) * * [1] If a file has no INI and sChars is not set, it will receive a default * mapping of ASCII or ISO-8859-1 if the font has exactly 128 or 256 frames. * However, if it doesn't, we don't know what it is and the font will receive * no default mapping. A font isn't useful with no characters mapped. */ void Font::Load(const CString &sFontOrTextureFilePath, CString sChars) { /* Check for recursion (recursive imports). */ { for(unsigned i = 0; i < LoadStack.size(); ++i) { if(LoadStack[i] == sFontOrTextureFilePath) { CString str = join("\n", LoadStack); str += "\n" + sFontOrTextureFilePath; RageException::Throw("Font import recursion detected\n%s", str.GetString()); } } LoadStack.push_back(sFontOrTextureFilePath); } /* The font is not already loaded. Figure out what we have. */ // LOG->Trace( "FontManager::LoadFont(%s).", sFontFilePath.GetString() ); path = sFontOrTextureFilePath; Chars = sChars; /* Get the filenames associated with this font. */ CStringArray TexturePaths; CString IniPath; GetFontPaths(sFontOrTextureFilePath, TexturePaths, IniPath); bool CapitalsOnly = false; /* If we have an INI, load it. */ IniFile ini; if( !IniPath.empty() ) { ini.SetPath( IniPath ); ini.ReadFile(); ini.RenameKey("Char Widths", "main"); ini.GetValueB( "main", "CapitalsOnly", CapitalsOnly ); } { /* If this is a top-level font (not a subfont), load the default font first. */ CStringArray ImportList; if(LoadStack.size() == 1) ImportList.push_back("default font"); /* Check to see if we need to import any other fonts. Do this * before loading this font, so any characters in this font * override imported characters. */ CString imports; ini.GetValue( "main", "import", imports ); split(imports, ",", ImportList, true); for(unsigned i = 0; i < ImportList.size(); ++i) { CString path = THEME->GetPathTo("Fonts", ImportList[i]); Font subfont; subfont.Load(path, ""); MergeFont(subfont); } } /* Load each font page. */ for(unsigned i = 0; i < TexturePaths.size(); ++i) { FontPage *fp = new FontPage; /* Grab the page name, eg "foo" from "Normal [foo].png". */ CString pagename = GetPageNameFromFileName(TexturePaths[i]); /* Load settings for this page from the INI. */ FontPageSettings cfg; LoadFontPageSettings(cfg, ini, TexturePaths[i], pagename, sChars); /* Go. */ fp->Load(cfg); /* Expect at least as many frames as we have premapped characters. */ /* Make sure that we don't map characters to frames we don't actually * have. This can happen if the font is too small for an sChars. */ for(map::const_iterator it = fp->m_iCharToGlyphNo.begin(); it != fp->m_iCharToGlyphNo.end(); ++it) { if(it->second < fp->m_pTexture->GetNumFrames()) continue; /* OK */ RageException::Throw( "The font '%s' maps %s to frame %i, but the font only has %i frames.", TexturePaths[i].GetString(), WcharDisplayText(wchar_t(it->first)).GetString(), it->second, fp->m_pTexture->GetNumFrames() ); } LOG->Trace("Adding page %s (%s) to %s; %i glyphs", TexturePaths[i].GetString(), pagename.GetString(), sFontOrTextureFilePath.GetString(), fp->m_iCharToGlyphNo.size()); AddPage(fp); /* If this is the first font loaded, or it's called "main", this page's * properties become the font's properties. */ if(i == 0 || pagename == "main") SetDefaultGlyph(fp); } if(CapitalsOnly) CapsOnly(); if(m_iCharToGlyph.empty()) LOG->Warn("Font %s has no characters", sFontOrTextureFilePath.GetString()); LoadStack.pop_back(); }