Files
itgmania212121/src/Font.cpp
T
Arthur Eubanks 995f0ea8c1 Change some RString methods to free functions
These ones aren't a std::string method. Doing this helps the RString to
std::string migration.
2025-05-17 14:02:12 -07:00

939 lines
29 KiB
C++

#include "global.h"
#include "Font.h"
#include "IniFile.h"
#include "RageTextureManager.h"
#include "RageUtil.h"
#include "RageLog.h"
#include "FontManager.h"
#include "ThemeManager.h"
#include "FontCharmaps.h"
#include "FontCharAliases.h"
#include "arch/Dialog/Dialog.h"
#include <cmath>
#include <cstddef>
#include <vector>
FontPage::FontPage(): m_iHeight(0), m_iLineSpacing(0), m_fVshift(0),
m_iDrawExtraPixelsLeft(0), m_iDrawExtraPixelsRight(0),
m_FontPageTextures(), m_sTexturePath(""), m_aGlyphs(),
m_iCharToGlyphNo() {}
void FontPage::Load( const FontPageSettings &cfg )
{
m_sTexturePath = cfg.m_sTexturePath;
// load texture
RageTextureID ID1( m_sTexturePath );
if( cfg.m_sTextureHints != "default" )
ID1.AdditionalTextureHints = cfg.m_sTextureHints;
m_FontPageTextures.m_pTextureMain = TEXTUREMAN->LoadTexture( ID1 );
if(m_FontPageTextures.m_pTextureMain == nullptr)
{
LuaHelpers::ReportScriptErrorFmt(
"Failed to load main texture %s for font page.",
m_sTexturePath.c_str());
m_FontPageTextures.m_pTextureMain= TEXTUREMAN->LoadTexture(
TEXTUREMAN->GetDefaultTextureID());
}
RageTextureID ID2 = ID1;
// "arial 20 16x16 [main].png" => "arial 20 16x16 [main-stroke].png"
if( ID2.filename.find("]") != std::string::npos )
{
Replace(ID2.filename, "]", "-stroke]");
if( IsAFile(ID2.filename) )
{
m_FontPageTextures.m_pTextureStroke = TEXTUREMAN->LoadTexture( ID2 );
if(m_FontPageTextures.m_pTextureStroke == nullptr)
{
LuaHelpers::ReportScriptErrorFmt(
"Failed to load stroke texture %s for font page.",
ID2.filename.c_str());
m_FontPageTextures.m_pTextureStroke=
m_FontPageTextures.m_pTextureMain;
}
// If the source frame dimensions or number of frames don't match, set
// the stroke layer to be the same as the main layer so that the
// assumptions based on the frames are still safe. -Kyz
if(m_FontPageTextures.m_pTextureMain->GetSourceFrameWidth() !=
m_FontPageTextures.m_pTextureStroke->GetSourceFrameWidth())
{
LuaHelpers::ReportScriptErrorFmt(
"'%s' and '%s' must have the same frame widths",
ID1.filename.c_str(), ID2.filename.c_str());
m_FontPageTextures.m_pTextureStroke=
m_FontPageTextures.m_pTextureMain;
}
if(m_FontPageTextures.m_pTextureMain->GetNumFrames() !=
m_FontPageTextures.m_pTextureStroke->GetNumFrames())
{
LuaHelpers::ReportScriptErrorFmt(
"'%s' and '%s' must have the same frame dimensions",
ID1.filename.c_str(), ID2.filename.c_str());
m_FontPageTextures.m_pTextureStroke=
m_FontPageTextures.m_pTextureMain;
}
}
}
// load character widths
std::vector<int> aiFrameWidths;
int default_width = m_FontPageTextures.m_pTextureMain->GetSourceFrameWidth();
if( cfg.m_iDefaultWidth != -1 )
default_width = cfg.m_iDefaultWidth;
// Assume each character is the width of the frame by default.
for( int i=0; i<m_FontPageTextures.m_pTextureMain->GetNumFrames(); i++ )
{
std::map<int, int>::const_iterator it = cfg.m_mapGlyphWidths.find(i);
if( it != cfg.m_mapGlyphWidths.end() )
aiFrameWidths.push_back( it->second );
else
aiFrameWidths.push_back( default_width );
}
if( cfg.m_iAddToAllWidths )
{
for( int i=0; i<m_FontPageTextures.m_pTextureMain->GetNumFrames(); i++ )
aiFrameWidths[i] += cfg.m_iAddToAllWidths;
}
if( cfg.m_fScaleAllWidthsBy != 1 )
{
for( int i=0; i<m_FontPageTextures.m_pTextureMain->GetNumFrames(); i++ )
aiFrameWidths[i] = std::lrint( aiFrameWidths[i] * cfg.m_fScaleAllWidthsBy );
}
m_iCharToGlyphNo = cfg.CharToGlyphNo;
m_iLineSpacing = cfg.m_iLineSpacing;
if( m_iLineSpacing == -1 )
m_iLineSpacing = m_FontPageTextures.m_pTextureMain->GetSourceFrameHeight();
int iBaseline=0;
/* If we don't have a top and/or baseline, assume we're centered in the
* frame, and that LineSpacing is the total height. */
iBaseline = cfg.m_iBaseline;
if( iBaseline == -1 )
{
float center = m_FontPageTextures.m_pTextureMain->GetSourceFrameHeight()/2.0f;
iBaseline = int( center + m_iLineSpacing/2 );
}
int iTop = cfg.m_iTop;
if( iTop == -1 )
{
float center = m_FontPageTextures.m_pTextureMain->GetSourceFrameHeight()/2.0f;
iTop = int( center - m_iLineSpacing/2 );
}
m_iHeight = iBaseline - iTop;
m_iDrawExtraPixelsLeft = cfg.m_iDrawExtraPixelsLeft;
m_iDrawExtraPixelsRight = cfg.m_iDrawExtraPixelsRight;
// Shift the character up so the top will be rendered at the baseline.
m_fVshift = (float) -iBaseline;
SetTextureCoords( aiFrameWidths, cfg.m_iAdvanceExtraPixels );
SetExtraPixels( cfg.m_iDrawExtraPixelsLeft, cfg.m_iDrawExtraPixelsRight );
// LOG->Trace("Font %s: height %i, baseline %i ( == top %i)",
// m_sTexturePath.c_str(), height, baseline, baseline-height);
}
void FontPage::SetTextureCoords( const std::vector<int> &widths, int iAdvanceExtraPixels )
{
for(int i = 0; i < m_FontPageTextures.m_pTextureMain->GetNumFrames(); ++i)
{
glyph g;
g.m_pPage = 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.m_TexRect = *m_FontPageTextures.m_pTextureMain->GetTextureCoordRect(i);
// Set the width and height to the width and line spacing, respectively.
g.m_fWidth = float( widths[i] );
g.m_fHeight = float(m_FontPageTextures.m_pTextureMain->GetSourceFrameHeight());
g.m_iHadvance = int(g.m_fWidth) + iAdvanceExtraPixels;
/* Do the same thing with X. Do this by changing the actual rendered
* m_TexRect, instead of shifting it, so we don't render more than we
* need to. */
g.m_fHshift = 0;
{
int iSourcePixelsToChopOff = m_FontPageTextures.m_pTextureMain->GetSourceFrameWidth() - widths[i];
if( (iSourcePixelsToChopOff % 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.
* Note that, since we set m_iHadvance above, this merely expands what
* we render; it doesn't advance the cursor further. So, glyphs
* that have an odd width should err to being a pixel offcenter left,
* not right. */
--iSourcePixelsToChopOff;
++g.m_fWidth;
}
const float fTexCoordsToChopOff = iSourcePixelsToChopOff * m_FontPageTextures.m_pTextureMain->GetSourceToTexCoordsRatioX();
g.m_TexRect.left += fTexCoordsToChopOff/2;
g.m_TexRect.right -= fTexCoordsToChopOff/2;
}
g.m_FontPageTextures = m_FontPageTextures;
m_aGlyphs.push_back(g);
}
}
void FontPage::SetExtraPixels( int iDrawExtraPixelsLeft, int iDrawExtraPixelsRight )
{
// Most fonts don't take the stroke into account, so if it shows up, it'll
// be cut off. I now understand why this code was here before. -freem
iDrawExtraPixelsRight++;
iDrawExtraPixelsLeft++;
if( (iDrawExtraPixelsLeft % 2) == 1 )
++iDrawExtraPixelsLeft;
// Adjust for iDrawExtraPixelsLeft and iDrawExtraPixelsRight.
for( unsigned i = 0; i < m_aGlyphs.size(); ++i )
{
int iFrameWidth = m_FontPageTextures.m_pTextureMain->GetSourceFrameWidth();
float fCharWidth = m_aGlyphs[i].m_fWidth;
/* Extra pixels to draw to the left and right. We don't have to
* worry about alignment here; fCharWidth is always even (by
* SetTextureCoords) and iFrameWidth are almost always even. */
float fExtraLeft = std::min( float(iDrawExtraPixelsLeft), (iFrameWidth-fCharWidth)/2.0f );
float fExtraRight = std::min( float(iDrawExtraPixelsRight), (iFrameWidth-fCharWidth)/2.0f );
// Move left and expand right.
m_aGlyphs[i].m_TexRect.left -= fExtraLeft * m_FontPageTextures.m_pTextureMain->GetSourceToTexCoordsRatioX();
m_aGlyphs[i].m_TexRect.right += fExtraRight * m_FontPageTextures.m_pTextureMain->GetSourceToTexCoordsRatioX();
m_aGlyphs[i].m_fHshift -= fExtraLeft;
m_aGlyphs[i].m_fWidth += fExtraLeft + fExtraRight;
}
}
FontPage::~FontPage()
{
if( m_FontPageTextures.m_pTextureMain != nullptr )
{
TEXTUREMAN->UnloadTexture( m_FontPageTextures.m_pTextureMain );
m_FontPageTextures.m_pTextureMain = nullptr;
}
if( m_FontPageTextures.m_pTextureStroke != nullptr )
{
TEXTUREMAN->UnloadTexture( m_FontPageTextures.m_pTextureStroke );
m_FontPageTextures.m_pTextureStroke = nullptr;
}
}
int Font::GetLineWidthInSourcePixels( const std::wstring &szLine ) const
{
int iLineWidth = 0;
for( unsigned i=0; i<szLine.size(); i++ )
iLineWidth += GetGlyph(szLine[i]).m_iHadvance;
return iLineWidth;
}
int Font::GetLineHeightInSourcePixels( const std::wstring &szLine ) const
{
int iLineHeight = 0;
// The height of a line is the height of its tallest used font page.
for( unsigned i=0; i<szLine.size(); i++ )
iLineHeight = std::max( iLineHeight, GetGlyph(szLine[i]).m_pPage->m_iHeight );
return iLineHeight;
}
// width is a pointer so that we can return the used width through it.
size_t Font::GetGlyphsThatFit(const std::wstring& line, int* width) const
{
if(*width == 0)
{
*width= GetLineWidthInSourcePixels(line);
return line.size();
}
int curr_width= 0;
size_t i= 0;
for(i= 0; i < line.size() && curr_width < *width; ++i)
{
curr_width+= GetGlyph(line[i]).m_iHadvance;
}
*width= curr_width;
return i;
}
Font::Font(): m_iRefCount(1), path(""), m_apPages(), m_pDefault(nullptr),
m_iCharToGlyph(), m_bRightToLeft(false), m_bDistanceField(false),
// strokes aren't shown by default, hence the Color.
m_DefaultStrokeColor(RageColor(0,0,0,0)), m_sChars("") {}
Font::~Font()
{
Unload();
}
void Font::Unload()
{
//LOG->Trace("Font:Unload '%s'",path.c_str());
for( unsigned i = 0; i < m_apPages.size(); ++i )
delete m_apPages[i];
m_apPages.clear();
m_iCharToGlyph.clear();
m_pDefault = nullptr;
/* Don't clear the refcount. We've unloaded, but that doesn't mean things
* aren't still pointing to us. */
}
void Font::Reload()
{
if(path.empty())
{
LuaHelpers::ReportScriptError("Cannot reload a font that has an empty path.");
return;
}
Unload();
Load( path, m_sChars );
}
void Font::AddPage( FontPage *m_pPage )
{
m_apPages.push_back( m_pPage );
for( std::map<wchar_t,int>::const_iterator it = m_pPage->m_iCharToGlyphNo.begin();
it != m_pPage->m_iCharToGlyphNo.end(); ++it )
{
m_iCharToGlyph[it->first] = &m_pPage->m_aGlyphs[it->second];
}
}
void Font::MergeFont(Font &f)
{
/* If we don't have a font page yet, and f does, grab the default font
* page. It'll usually be overridden later on by one of our own font
* pages; this will be used only if we don't have any font pages at
* all. */
if( m_pDefault == nullptr )
m_pDefault = f.m_pDefault;
for(std::map<wchar_t,glyph*>::iterator it = f.m_iCharToGlyph.begin();
it != f.m_iCharToGlyph.end(); ++it)
{
m_iCharToGlyph[it->first] = it->second;
}
m_apPages.insert( m_apPages.end(), f.m_apPages.begin(), f.m_apPages.end() );
f.m_apPages.clear();
}
const glyph &Font::GetGlyph( wchar_t c ) const
{
/* XXX: This is kind of nasty, but the parts that touch this are dark and
* scary. --Colby
*
* Snagged from OpenITG, original comment:
* shooting a blank really...DarkLink kept running into the stupid assert
* with non-roman song titles, and looking at it, I'm gonna guess that
* this is how ITG2 prevented crashing with them --infamouspat */
//ASSERT(c >= 0 && c <= 0xFFFFFF);
// wchar_t can be signed or unsigned depending on the platform and the compiler.
// We use WCHAR_MIN to determine a valid condition that won't emit a type-limits warning.
#if WCHAR_MIN != 0
if (c < 0 || c > 0xFFFFFF) {
#else
if (c > 0xFFFFFF) {
#endif
c = 1;
}
// Fast path:
if( c < (int) ARRAYLEN(m_iCharToGlyphCache) && m_iCharToGlyphCache[c] )
return *m_iCharToGlyphCache[c];
// Try the regular character.
std::map<wchar_t, glyph*>::const_iterator it = m_iCharToGlyph.find(c);
// If that's missing, use the default glyph.
if(it == m_iCharToGlyph.end())
it = m_iCharToGlyph.find(FONT_DEFAULT_GLYPH);
if(it == m_iCharToGlyph.end())
RageException::Throw( "The default glyph is missing from the font \"%s\".", path.c_str() );
return *it->second;
}
bool Font::FontCompleteForString( const std::wstring &str ) const
{
std::map<wchar_t, glyph*>::const_iterator mapDefault = m_iCharToGlyph.find( FONT_DEFAULT_GLYPH );
if( mapDefault == m_iCharToGlyph.end() )
RageException::Throw( "The default glyph is missing from the font \"%s\".", path.c_str() );
for( unsigned i = 0; i < str.size(); ++i )
{
// If the glyph for this character is the default glyph, we're incomplete.
const glyph &g = GetGlyph( str[i] );
if( &g == mapDefault->second )
return false;
}
return true;
}
void Font::CapsOnly()
{
/* For each uppercase character that we have a mapping for, add
* a lowercase one. */
for( char c = 'A'; c <= 'Z'; ++c )
{
std::map<wchar_t, glyph*>::const_iterator it = m_iCharToGlyph.find(c);
if(it == m_iCharToGlyph.end())
continue;
m_iCharToGlyph[(char) tolower(c)] = it->second;
}
}
void Font::SetDefaultGlyph( FontPage *pPage )
{
ASSERT( pPage != nullptr );
if(pPage->m_aGlyphs.empty())
{
LuaHelpers::ReportScriptErrorFmt(
"Attempted to set default glyphs for %s to a blank page.",
path.c_str());
return;
}
m_pDefault = pPage;
}
// Given the INI for a font, find all of the texture pages for the font.
void Font::GetFontPaths( const RString &sFontIniPath, std::vector<RString> &asTexturePathsOut )
{
RString sPrefix = SetExtension( sFontIniPath, "" );
std::vector<RString> asFiles;
GetDirListing( sPrefix + "*", asFiles, false, true );
for( unsigned i = 0; i < asFiles.size(); ++i )
{
if( !EqualsNoCase(Right(asFiles[i], 4), ".ini") )
asTexturePathsOut.push_back( asFiles[i] );
}
}
RString Font::GetPageNameFromFileName( const RString &sFilename )
{
size_t begin = sFilename.find_first_of( '[' );
if( begin == std::string::npos )
return "main";
size_t end = sFilename.find_first_of( ']', begin );
if( end == std::string::npos )
return "main";
begin++;
end--;
if( end == begin )
return "main";
return sFilename.substr( begin, end-begin+1 );
}
void Font::LoadFontPageSettings( FontPageSettings &cfg, IniFile &ini, const RString &sTexturePath, const RString &sPageName, RString sChars )
{
cfg.m_sTexturePath = sTexturePath;
// If we have any characters to map, add them.
for( unsigned n=0; n<sChars.size(); n++ )
{
char c = sChars[n];
cfg.CharToGlyphNo[c] = n;
}
int num_frames_wide, num_frames_high;
RageTexture::GetFrameDimensionsFromFileName( sTexturePath, &num_frames_wide, &num_frames_high );
int iNumFrames = num_frames_wide * num_frames_high;
// Deal with fonts generated by Bitmap Font Builder.
ini.RenameKey("Char Widths", "main");
// LOG->Trace("Loading font page '%s' settings from page name '%s'",
// TexturePath.c_str(), sPageName.c_str());
ini.GetValue( sPageName, "DrawExtraPixelsLeft", cfg.m_iDrawExtraPixelsLeft );
ini.GetValue( sPageName, "DrawExtraPixelsRight", cfg.m_iDrawExtraPixelsRight );
ini.GetValue( sPageName, "AddToAllWidths", cfg.m_iAddToAllWidths );
ini.GetValue( sPageName, "ScaleAllWidthsBy", cfg.m_fScaleAllWidthsBy );
ini.GetValue( sPageName, "LineSpacing", cfg.m_iLineSpacing );
ini.GetValue( sPageName, "Top", cfg.m_iTop );
ini.GetValue( sPageName, "Baseline", cfg.m_iBaseline );
ini.GetValue( sPageName, "DefaultWidth", cfg.m_iDefaultWidth );
ini.GetValue( sPageName, "AdvanceExtraPixels", cfg.m_iAdvanceExtraPixels );
ini.GetValue( sPageName, "TextureHints", cfg.m_sTextureHints );
// Iterate over all keys.
const XNode* pNode = ini.GetChild( sPageName );
if( pNode )
{
FOREACH_CONST_Attr( pNode, pAttr )
{
RString sName = pAttr->first;
const XNodeValue *pValue = pAttr->second;
MakeUpper(sName);
// If val is an integer, it's a width, eg. "10=27".
if( IsAnInt(sName) )
{
cfg.m_mapGlyphWidths[StringToInt(sName)] = pValue->GetValue<int>();
continue;
}
// "map codepoint=frame" maps a char to a frame.
if( sName.substr(0, 4) == "MAP " )
{
/* map CODEPOINT=frame. CODEPOINT can be
* 1. U+hexval
* 2. an alias ("oq")
* 3. a character in quotes ("X")
*
* map 1=2 is the same as
* range unicode #1-1=2
*/
RString sCodepoint = sName.substr(4); // "CODEPOINT"
wchar_t c;
if( sCodepoint.substr(0, 2) == "U+" && IsHexVal(sCodepoint.substr(2)) )
sscanf( sCodepoint.substr(2).c_str(), "%lc", &c );
else if( sCodepoint.size() > 0 &&
utf8_get_char_len(sCodepoint[0]) == int(sCodepoint.size()) )
{
c = utf8_get_char( sCodepoint.c_str() );
if(c == wchar_t(-1))
LOG->Warn("Font definition '%s' has an invalid value '%s'.",
ini.GetPath().c_str(), sName.c_str() );
}
else if( !FontCharAliases::GetChar(sCodepoint, c) )
{
LOG->Warn("Font definition '%s' has an invalid value '%s'.",
ini.GetPath().c_str(), sName.c_str() );
continue;
}
cfg.CharToGlyphNo[c] = pValue->GetValue<int>();
continue;
}
if( sName.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
*/
std::vector<RString> asMatches;
static Regex parse("^RANGE ([A-Z0-9\\-]+)( ?#([0-9A-F]+)-([0-9A-F]+))?$");
bool match = parse.Compare( sName, asMatches );
ASSERT( asMatches.size() == 4 ); // 4 parens
if(!match || asMatches[0].empty())
{
LuaHelpers::ReportScriptErrorFmt(
"Font definition \"%s\" has an invalid range \"%s\": parse error.",
ini.GetPath().c_str(), sName.c_str());
continue;
}
// We must have either 1 match (just the codeset) or 4 (the whole thing).
int count = -1;
unsigned int first = 0;
if( !asMatches[2].empty() )
{
sscanf( asMatches[2].c_str(), "%x", &first );
unsigned int last;
sscanf( asMatches[3].c_str(), "%x", &last );
if(last < first)
{
LuaHelpers::ReportScriptErrorFmt(
"Font definition \"%s\" has an invalid range \"%s\": %i < %i.",
ini.GetPath().c_str(), sName.c_str(), last, first);
continue;
}
count = last - first + 1;
}
RString error_string = cfg.MapRange( asMatches[0], first, pValue->GetValue<int>(), count );
if(!error_string.empty())
{
LuaHelpers::ReportScriptErrorFmt(
"Font definition \"%s\" has an invalid range \"%s\": %s.",
ini.GetPath().c_str(), sName.c_str(), error_string.c_str());
continue;
}
continue;
}
if( sName.substr(0, 5) == "LINE " )
{
/* line ROW=CHAR1CHAR2CHAR3CHAR4
* eg.
* line 0=ABCDEFGH
*
* This lets us assign characters very compactly and readably. */
RString row_str = sName.substr(5);
TrimLeft(row_str);
if(!IsAnInt(row_str))
{
LuaHelpers::ReportScriptErrorFmt("Line name %s is not a number.",
row_str.c_str());
continue;
}
const int row = StringToInt(row_str);
const int first_frame = row * num_frames_wide;
if(row >= num_frames_high)
{
LuaHelpers::ReportScriptErrorFmt(
"The font definition \"%s\" tries to assign line %i, "
"but the font is only %i characters high. "
"Line numbers start at 0.",
ini.GetPath().c_str(), first_frame, num_frames_high);
continue;
}
// Decode the string.
const std::wstring wdata( RStringToWstring(pValue->GetValue<RString>()) );
if(int(wdata.size()) > num_frames_wide)
{
LuaHelpers::ReportScriptErrorFmt(
"The font definition \"%s\" assigns %i characters to row %i"
"(\"%ls\"), but the font is only %i characters wide.",
ini.GetPath().c_str(), (int)wdata.size(), row, wdata.c_str(),
num_frames_wide);
continue;
}
for( unsigned i = 0; i < wdata.size(); ++i )
cfg.CharToGlyphNo[wdata[i]] = first_frame+i;
}
}
}
/* If it's 128 or 256 frames, default to ASCII or CP1252,
* respectively. 5x3 and 4x4 numbers fonts are supported as well.
* If it's anything else, we don't know what it is, so don't make
* any default mappings (the INI needs to do that itself). */
if( sPageName != "common" && cfg.CharToGlyphNo.empty() )
{
switch( iNumFrames )
{
case 128:
cfg.MapRange( "ascii", 0, 0, -1 );
break;
case 256:
cfg.MapRange( "cp1252", 0, 0, -1 );
break;
case 15:
case 16:
cfg.MapRange( "numbers", 0, 0, -1 );
break;
default:
LOG->Trace( "Font page \"%s\" has no characters", sTexturePath.c_str() );
}
}
// If ' ' is set and nbsp is not, set nbsp.
if( cfg.CharToGlyphNo.find(' ') != cfg.CharToGlyphNo.end() )
cfg.CharToGlyphNo[0x00A0] = cfg.CharToGlyphNo[' '];
}
RString FontPageSettings::MapRange( RString sMapping, int iMapOffset, int iGlyphNo, int iCount )
{
if( !CompareNoCase(sMapping, "Unicode") )
{
// Special case.
if( iCount == -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( iCount > 16384 )
return ssprintf( "Can't map %i glyphs to one font page", iCount );
while( iCount )
{
CharToGlyphNo[iMapOffset] = iGlyphNo;
iMapOffset++;
iGlyphNo++;
iCount--;
}
return RString();
}
const wchar_t *pMapping = FontCharmaps::get_char_map( sMapping );
if( pMapping == nullptr )
return "Unknown mapping";
while( *pMapping != 0 && iMapOffset )
{
pMapping++;
--iMapOffset;
}
if( iMapOffset )
return "Map overflow"; // there aren't enough characters in the map
// If iCount is -1, set it to the number of characters in the map.
if( iCount == -1 )
for( iCount = 0; pMapping[iCount] != 0; ++iCount ) ;
while( *pMapping != 0 )
{
if( *pMapping != FontCharmaps::M_SKIP )
CharToGlyphNo[*pMapping] = iGlyphNo;
pMapping++;
iGlyphNo++;
iCount--;
}
if( iCount )
return "Map overflow"; // there aren't enough characters in the map
return RString();
}
static std::vector<RString> 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)
*
* One INI and at least one texture is required.
*
* The entire font can be redirected; that's handled in ThemeManager.
* Individual font files can not be redirected.
*
* If a file has no characters 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.
* 5x3 (15 frames) and 4x4 (16 frames) numbers sheets are also supported.
* 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 RString &sIniPath, RString sChars )
{
if(CompareNoCase(GetExtension(sIniPath), "ini"))
{
LuaHelpers::ReportScriptErrorFmt(
"%s is not an ini file. Fonts can only be loaded from ini files.",
sIniPath.c_str());
return;
}
//LOG->Trace( "Font: Loading new font '%s'",sIniPath.c_str());
// Check for recursion (recursive imports).
for( unsigned i = 0; i < LoadStack.size(); ++i )
{
if( LoadStack[i] == sIniPath )
{
RString str = join("\n", LoadStack);
str += "\nCurrent font: " + sIniPath;
LuaHelpers::ReportScriptErrorFmt(
"Font import recursion detected\n%s", str.c_str());
return;
}
}
LoadStack.push_back( sIniPath );
// The font is not already loaded. Figure out what we have.
CHECKPOINT_M( ssprintf("Font::Load(\"%s\",\"%s\").", sIniPath.c_str(), m_sChars.c_str()) );
path = sIniPath;
m_sChars = sChars;
// Get the filenames associated with this font.
std::vector<RString> asTexturePaths;
GetFontPaths( sIniPath, asTexturePaths );
bool bCapitalsOnly = false;
// If we have an INI, load it.
IniFile ini;
if( !sIniPath.empty() )
{
ini.ReadFile( sIniPath );
ini.RenameKey("Char Widths", "main"); // backward compat
ini.GetValue( "common", "CapitalsOnly", bCapitalsOnly );
ini.GetValue( "common", "RightToLeft", m_bRightToLeft );
ini.GetValue( "common", "DistanceField", m_bDistanceField );
RString s;
if( ini.GetValue( "common", "DefaultStrokeColor", s ) )
m_DefaultStrokeColor.FromString( s );
}
{
std::vector<RString> ImportList;
bool bIsTopLevelFont = LoadStack.size() == 1;
// If this is a top-level font (not a subfont), load the default font first.
if( bIsTopLevelFont )
ImportList.push_back("Common default");
/* 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. */
RString imports;
ini.GetValue( "main", "import", imports );
split(imports, ",", ImportList, true);
if( bIsTopLevelFont && imports.empty() && asTexturePaths.empty() )
{
RString s = ssprintf( "Font \"%s\" is a top-level font with no textures or imports.", sIniPath.c_str() );
Dialog::OK( s );
}
for(unsigned i = 0; i < ImportList.size(); ++i)
{
RString sPath = THEME->GetPathF( "", ImportList[i], true );
if( sPath == "" )
{
RString s = ssprintf( "Font \"%s\" imports a font \"%s\" that doesn't exist", sIniPath.c_str(), ImportList[i].c_str() );
Dialog::OK( s );
continue;
}
Font subfont;
subfont.Load(sPath,"");
MergeFont(subfont);
}
}
// Load each font page.
for( unsigned i = 0; i < asTexturePaths.size(); ++i )
{
const RString &sTexturePath = asTexturePaths[i];
// Grab the page name, eg "foo" from "Normal [foo].png".
RString sPagename = GetPageNameFromFileName( sTexturePath );
// Ignore stroke textures
if( sTexturePath.find("-stroke") != std::string::npos )
continue;
// Create this down here so it doesn't leak if the continue gets triggered.
FontPage *pPage = new FontPage;
// Load settings for this page from the INI.
FontPageSettings cfg;
LoadFontPageSettings( cfg, ini, sTexturePath, "common", sChars );
LoadFontPageSettings( cfg, ini, sTexturePath, sPagename, sChars );
// Load.
pPage->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(std::map<wchar_t,int>::iterator it = pPage->m_iCharToGlyphNo.begin();
it != pPage->m_iCharToGlyphNo.end(); ++it)
{
if( it->second < pPage->m_FontPageTextures.m_pTextureMain->GetNumFrames() )
continue; /* OK */
LuaHelpers::ReportScriptErrorFmt(
"The font \"%s\" maps \"%s\" to frame %i, "
"but the font only has %i frames.",
sTexturePath.c_str(), WcharDisplayText(wchar_t(it->first)).c_str(),
it->second,
pPage->m_FontPageTextures.m_pTextureMain->GetNumFrames());
it->second= 0;
}
// LOG->Trace( "Adding page %s (%s) to %s; %i glyphs",
// TexturePaths[i].c_str(), sPagename.c_str(),
// sFontOrTextureFilePath.c_str(), pPage->m_iCharToGlyphNo.size() );
AddPage( pPage );
/* If this is the first font loaded, or it's called "main", this page's
* properties become the font's properties. */
if( i == 0 || sPagename == "main" )
SetDefaultGlyph( pPage );
}
if( bCapitalsOnly )
CapsOnly();
if( m_iCharToGlyph.empty() )
LuaHelpers::ReportScriptErrorFmt("Font %s has no characters", sIniPath.c_str());
LoadStack.pop_back();
if( LoadStack.empty() )
{
// Cache ASCII glyphs.
ZERO( m_iCharToGlyphCache );
std::map<wchar_t,glyph*>::iterator it;
for( it = m_iCharToGlyph.begin(); it != m_iCharToGlyph.end(); ++it )
if( it->first < (int) ARRAYLEN(m_iCharToGlyphCache) )
m_iCharToGlyphCache[it->first] = it->second;
}
}
/*
* (c) 2001-2004 Glenn Maynard, Chris Danford
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/