openclonk/src/graphics/C4FontLoader.cpp

1075 lines
32 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 2003-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2009-2013, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
// text drawing facility for C4Draw
#include <C4Include.h>
#include "C4FontLoader.h"
#ifndef USE_CONSOLE
#include <C4Components.h>
#include <C4Config.h>
#include <C4Draw.h>
#include <C4Language.h>
#include <C4Log.h>
#include <C4Markup.h>
#include <C4Surface.h>
#include <stdexcept>
#include <string>
#ifdef _WIN32
#include <C4windowswrapper.h>
#endif
#include <ft2build.h>
#include FT_FREETYPE_H
#endif
/* Initialization */
bool C4FontLoader::InitFont(CStdFont * rFont, const char *szFontName, FontType eType, int32_t iSize, C4GroupSet *pGfxGroups, bool fDoShadow)
{
#ifdef USE_CONSOLE
return true;
#else
// safety
assert(szFontName);
if (!szFontName || !*szFontName)
{
LogFatal(FormatString("%s (\"%s\")", LoadResStr("IDS_ERR_INITFONTS"), szFontName ? szFontName : "(null)").getData());
return false;
}
// if def has not been found, use the def as font name
// determine font def string
const char *szFontString = szFontName;
// font not assigned?
assert(*szFontString);
if (!*szFontString)
{
// invalid call or spec
LogFatal(LoadResStr("IDS_ERR_INITFONTS")); return false;
}
// get font name
char FontFaceName[C4MaxName+1], FontParam[C4MaxName+1];
SCopyUntil(szFontString, FontFaceName, ',', C4MaxName);
// is it an image file?
int32_t iDefFontSize; DWORD dwDefWeight=FW_NORMAL;
switch (eType)
{
case C4FT_Log: iDefFontSize = iSize*12/14; break;
case C4FT_MainSmall:iDefFontSize = iSize*13/14; break;
case C4FT_Main: iDefFontSize = iSize; break;
case C4FT_Caption: iDefFontSize = iSize*16/14; break;
case C4FT_Title: iDefFontSize = iSize*22/14; break;
default: assert(false); LogFatal(LoadResStr("IDS_ERR_INITFONTS")); return false; // invalid call
}
// regular font name: let WinGDI or Freetype draw a font with the given parameters
// font size given?
if (SCopySegment(szFontString, 1, FontParam, ',', C4MaxName))
sscanf(FontParam, "%i", &iDefFontSize);
// font weight given?
if (SCopySegment(szFontString, 2, FontParam, ',', C4MaxName))
{
int iDefWeight;
sscanf(FontParam, "%i", &iDefWeight);
dwDefWeight = iDefWeight;
}
// check if it's already loaded from that group with that parameters
if (rFont->IsSameAs(FontFaceName, iDefFontSize, dwDefWeight))
return true;
// it's not; so (re-)load it now!
if (rFont->IsInitialized())
{
// reloading
rFont->Clear();
LogF(LoadResStr("IDS_PRC_UPDATEFONT"), FontFaceName, iDefFontSize, dwDefWeight);
}
// check if one of the internally listed fonts should be used
const char * const extensions[] = { "ttf", "otf", "ttc", "fon", "fnt", "fot", NULL };
char FileName[_MAX_PATH+1];
int32_t ID;
C4Group * pGrp = pGfxGroups->FindSuitableFile(FontFaceName, extensions, FileName, &ID);
if (pGrp)
{
if (LastUsedGrpID != ID || LastUsedName != FontFaceName)
{
DestroyFont(pLastUsedFont);
pLastUsedFont = NULL;
}
if (!pLastUsedFont)
{
StdBuf Data;
if (pGrp->LoadEntry(FileName, &Data))
{
try
{
pLastUsedFont = CreateFont(Data);
LastUsedGrpID = ID;
LastUsedName = FontFaceName;
}
catch (std::runtime_error & e)
{
LogFatal(e.what());
pGrp = NULL;
}
}
}
}
// no internal font match? Then create one using the given face/filename (using a system font)
if (!pGrp)
{
if (LastUsedGrpID != -1 || LastUsedName != FontFaceName)
{
DestroyFont(pLastUsedFont);
pLastUsedFont = NULL;
}
if (!pLastUsedFont)
{
try
{
pLastUsedFont = CreateFont(FontFaceName);
if (!pLastUsedFont)
// no match for font face found
throw std::runtime_error(FormatString("Font face %s undefined", FontFaceName).getData());
LastUsedGrpID = -1;
LastUsedName = FontFaceName;
}
catch (std::runtime_error & e)
{
LogFatal(e.what());
}
}
}
if (!pLastUsedFont)
{
LogFatal(LoadResStr("IDS_ERR_INITFONTS"));
return false;
}
try
{
rFont->Init(*pLastUsedFont, FontFaceName, iDefFontSize, dwDefWeight, fDoShadow); // throws exception on error
return true;
}
catch (std::runtime_error & e)
{
LogFatal(e.what());
LogFatal(LoadResStr("IDS_ERR_INITFONTS"));
return false;
}
#endif
}
void C4FontLoader::Clear()
{
#ifndef USE_CONSOLE
// delete vector font cache
DestroyFont(pLastUsedFont);
pLastUsedFont = NULL;
#endif
}
#ifndef USE_CONSOLE
class CStdVectorFont
{
FT_Library library;
FT_Face face;
StdBuf Data;
public:
CStdVectorFont(const char * FontFaceName): RefCnt(1)
{
#if defined(_WIN32)
// Win32 using freetype: Load TrueType-data from WinGDI into Data-buffer to be used by FreeType
bool fSuccess = false;
HDC hDC = ::CreateCompatibleDC(NULL);
if (hDC)
{
HFONT hFont = ::CreateFontA(0, 0, 0, 0, FW_DONTCARE, false,
false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, 5,
VARIABLE_PITCH, FontFaceName);
if (hFont)
{
SelectObject( hDC, hFont );
uint32_t dwTTFSize = ::GetFontData(hDC, 0, 0, NULL, 0);
if (dwTTFSize && dwTTFSize != GDI_ERROR)
{
Data.SetSize(dwTTFSize);
uint32_t dwRealTTFSize = ::GetFontData(hDC, 0, 0, Data.getMData(), dwTTFSize);
if (dwRealTTFSize == dwTTFSize)
{
fSuccess = true;
}
else
Data.Clear();
}
DeleteObject(hFont);
}
DeleteDC(hDC);
}
if (!fSuccess)
throw std::runtime_error("Some Win32 error");
// Initialize Freetype
if (FT_Init_FreeType(&library))
throw std::runtime_error("Cannot init Freetype");
// Load the font
FT_Error e;
if ((e=FT_New_Memory_Face(library, static_cast<const FT_Byte *>(Data.getData()), Data.getSize(), 0, &face)))
throw std::runtime_error(std::string("Cannot load font: ") + FormatString("%d",e).getData());
#else
//FIXME: get path name from OS
// Initialize Freetype
if (FT_Init_FreeType(&library))
throw std::runtime_error("Cannot init Freetype");
// Load the font
FT_Error e;
if ((e=FT_New_Face(library, FontFaceName, 0, &face)))
throw std::runtime_error(std::string("Cannot load ") + FontFaceName + ": " + FormatString("%d",e).getData());
#endif
}
CStdVectorFont(StdBuf & Data) : Data(Data), RefCnt(1)
{
// Initialize Freetype
if (FT_Init_FreeType(&library))
throw std::runtime_error("Cannot init Freetype");
// Load the font
FT_Error e;
if ((e=FT_New_Memory_Face(library, static_cast<const FT_Byte *>(Data.getData()), Data.getSize(), 0, &face)))
throw std::runtime_error(std::string("Cannot load font: ") + FormatString("%d",e).getData());
}
~CStdVectorFont()
{
FT_Done_Face(face);
FT_Done_FreeType(library);
}
operator FT_Face () { return face; }
FT_Face operator -> () { return face; }
int RefCnt;
};
CStdVectorFont * C4FontLoader::CreateFont(const char *szFaceName)
{
return new CStdVectorFont(szFaceName);
}
CStdVectorFont * C4FontLoader::CreateFont(StdBuf & Data)
{
return new CStdVectorFont(Data);
}
void C4FontLoader::DestroyFont(CStdVectorFont * pFont)
{
if (!pFont) return;
--(pFont->RefCnt);
if (!pFont->RefCnt)
delete pFont;
}
#endif
C4FontLoader FontLoader;
CStdFont::CStdFont()
{
#ifndef USE_CONSOLE
// set default values
psfcFontData = NULL;
sfcCurrent = NULL;
iNumFontSfcs = 0;
iSfcSizes = 64;
dwDefFontHeight=iLineHgt=10;
iFontZoom=1; // default: no internal font zooming - likely no antialiasing either...
iHSpace=-1;
iGfxLineHgt=iLineHgt+1;
dwWeight=FW_NORMAL;
fDoShadow=false;
// font not yet initialized
*szFontName=0;
id=0;
pCustomImages=NULL;
pVectorFont = NULL;
#endif
}
#ifndef USE_CONSOLE
bool CStdFont::AddSurface()
{
// add new surface as render target; copy old ones
C4Surface **pNewSfcs = new C4Surface *[iNumFontSfcs+1];
if (iNumFontSfcs) memcpy(pNewSfcs, psfcFontData, iNumFontSfcs * sizeof (C4Surface *));
delete [] psfcFontData;
psfcFontData = pNewSfcs;
C4Surface *sfcNew = psfcFontData[iNumFontSfcs] = new C4Surface();
++iNumFontSfcs;
if (iSfcSizes) if (!sfcNew->Create(iSfcSizes, iSfcSizes)) return false;
// If old surface was locked, unlock it and lock the new one in its stead
if (sfcCurrent && sfcCurrent->IsLocked())
{
sfcCurrent->Unlock();
sfcNew->Lock();
}
sfcCurrent = sfcNew;
iCurrentSfcX = iCurrentSfcY = 0;
return true;
}
bool CStdFont::CheckRenderedCharSpace(uint32_t iCharWdt, uint32_t iCharHgt)
{
// need to do a line break?
if (iCurrentSfcX + iCharWdt >= (uint32_t)iSfcSizes) if (iCurrentSfcX)
{
iCurrentSfcX = 0;
iCurrentSfcY += iCharHgt;
if (iCurrentSfcY + iCharHgt >= (uint32_t)iSfcSizes)
{
// surface is full: Next one
if (!AddSurface()) return false;
}
}
// OK draw it there
return true;
}
bool CStdFont::AddRenderedChar(uint32_t dwChar, C4Facet *pfctTarget)
{
if (!pVectorFont) return false;
// Freetype character rendering
FT_Set_Pixel_Sizes(*pVectorFont, dwDefFontHeight, dwDefFontHeight);
int32_t iBoldness = dwWeight-400; // zero is normal; 300 is bold
if (iBoldness)
{
iBoldness = (1<<16) + (iBoldness<<16)/400;
FT_Matrix mat;
mat.xx = iBoldness; mat.xy = mat.yx = 0; mat.yy = 1<<16;
FT_Set_Transform(*pVectorFont, &mat, NULL);
}
else
{
FT_Set_Transform(*pVectorFont, NULL, NULL);
}
// Render
if (FT_Load_Char(*pVectorFont, dwChar, FT_LOAD_RENDER | FT_LOAD_NO_HINTING))
{
// although the character was not drawn, assume it's not in the font and won't be needed
// so return success here
return true;
}
// Make a shortcut to the glyph
FT_GlyphSlot slot = (*pVectorFont)->glyph;
if (slot->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
{
// although the character was drawn in a strange way, assume it's not in the font and won't be needed
// so return success here
return true;
}
// linebreak/ new surface check
int width = Max<int>(slot->advance.x / 64, Max(slot->bitmap_left,0) + slot->bitmap.width) + fDoShadow;
if (!CheckRenderedCharSpace(width, iGfxLineHgt)) return false;
// offset from the top
int at_y = iCurrentSfcY + dwDefFontHeight * (*pVectorFont)->ascender / (*pVectorFont)->units_per_EM - slot->bitmap_top;
int at_x = iCurrentSfcX + Max(slot->bitmap_left,0);
// Copy to the surface
if (!sfcCurrent->Lock()) return false;
for (unsigned int y = 0; y < slot->bitmap.rows + fDoShadow; ++y)
{
for (unsigned int x = 0; x < slot->bitmap.width + fDoShadow; ++x)
{
unsigned char bAlpha, bAlphaShadow;
if (x < slot->bitmap.width && y < slot->bitmap.rows)
bAlpha = (unsigned char)(slot->bitmap.buffer[slot->bitmap.width * y + x]);
else
bAlpha = 0;
// Make a shadow from the upper-left pixel, and blur with the eight neighbors
DWORD dwPixVal = 0u;
bAlphaShadow = 0;
if ((x || y) && fDoShadow)
{
int iShadow = 0;
if (x < slot->bitmap.width && y < slot->bitmap.rows) iShadow += slot->bitmap.buffer[(x - 0) + slot->bitmap.width * (y - 0)];
if (x > 1 && y < slot->bitmap.rows) iShadow += slot->bitmap.buffer[(x - 2) + slot->bitmap.width * (y - 0)];
if (x > 0 && y < slot->bitmap.rows) iShadow += slot->bitmap.buffer[(x - 1) + slot->bitmap.width * (y - 0)];
if (x < slot->bitmap.width && y > 1 ) iShadow += slot->bitmap.buffer[(x - 0) + slot->bitmap.width * (y - 2)];
if (x > 1 && y > 1 ) iShadow += slot->bitmap.buffer[(x - 2) + slot->bitmap.width * (y - 2)];
if (x > 0 && y > 1 ) iShadow += slot->bitmap.buffer[(x - 1) + slot->bitmap.width * (y - 2)];
if (x < slot->bitmap.width && y > 0 ) iShadow += slot->bitmap.buffer[(x - 0) + slot->bitmap.width * (y - 1)];
if (x > 1 && y > 0 ) iShadow += slot->bitmap.buffer[(x - 2) + slot->bitmap.width * (y - 1)];
if (x > 0 && y > 0 ) iShadow += slot->bitmap.buffer[(x - 1) + slot->bitmap.width * (y - 1)]*8;
bAlphaShadow += iShadow / 16;
// because blitting on a black pixel reduces luminosity as compared to shadowless font,
// assume luminosity as if blitting shadowless font on a 50% gray background
unsigned char cBack = bAlpha;
dwPixVal = RGBA(cBack/2, cBack/2, cBack/2, bAlphaShadow);
}
BltAlpha(dwPixVal, bAlpha << 24 | 0xffffff);
sfcCurrent->SetPixDw(at_x + x, at_y + y, dwPixVal);
}
}
sfcCurrent->Unlock();
// Save the position of the glyph for the rendering code
pfctTarget->Set(sfcCurrent, iCurrentSfcX, iCurrentSfcY, width, iGfxLineHgt);
// advance texture position
iCurrentSfcX += pfctTarget->Wdt;
return true;
}
C4Facet &CStdFont::GetUnicodeCharacterFacet(uint32_t c)
{
// find/add facet in map
C4Facet &rFacet = fctUnicodeMap[c];
// create character on the fly if necessary and possible
if (!rFacet.Surface) AddRenderedChar(c, &rFacet);
// rendering might have failed, in which case rFacet remains empty. Should be OK; char won't be printed then
return rFacet;
}
#endif
void CStdFont::Init(CStdVectorFont & VectorFont, const char *font_face_name, DWORD dwHeight, DWORD dwFontWeight, bool fDoShadow)
{
#ifndef USE_CONSOLE
// clear previous
Clear();
// set values
iHSpace=fDoShadow ? -1 : 0; // horizontal shadow
dwWeight=dwFontWeight;
this->fDoShadow = fDoShadow;
// determine needed texture size
if (dwHeight * iFontZoom > 40)
iSfcSizes = 512;
else if (dwDefFontHeight * iFontZoom > 20)
iSfcSizes = 256;
else
iSfcSizes = 128;
SCopy(font_face_name, szFontName, 80);
dwDefFontHeight = dwHeight;
// create surface
if (!AddSurface())
{
Clear();
throw std::runtime_error(std::string("Cannot create surface (") + szFontName + ")");
}
// Store vector font
pVectorFont = &VectorFont;
++(pVectorFont->RefCnt);
// Get size
// FIXME: use bbox or dynamically determined line heights here
iLineHgt = (VectorFont->ascender - VectorFont->descender) * dwHeight / VectorFont->units_per_EM;
iGfxLineHgt = iLineHgt + fDoShadow; // vertical shadow
// loop through all ASCII printable characters and prepare them
// Non-ASCII Unicode characters will be created on the fly
// now render all characters!
int cMax = 127;
sfcCurrent->Lock();
for (int c=' '; c <= cMax; ++c)
{
if (!AddRenderedChar(c, &(fctAsciiTexCoords[c-' '])))
{
sfcCurrent->Unlock();
Clear();
throw std::runtime_error(std::string("Cannot render characters for Font (") + szFontName + ")");
}
}
sfcCurrent->Unlock();
// adjust line height
iLineHgt /= iFontZoom;
#endif
}
void CStdFont::Clear()
{
#ifndef USE_CONSOLE
FontLoader.DestroyFont(pVectorFont);
pVectorFont = NULL;
// clear font sfcs
if (psfcFontData)
{
while (iNumFontSfcs--) delete psfcFontData[iNumFontSfcs];
delete [] psfcFontData;
psfcFontData = NULL;
}
sfcCurrent = NULL;
iNumFontSfcs = 0;
for (int c=' '; c<256; ++c) fctAsciiTexCoords[c-' '].Default();
fctUnicodeMap.clear();
// set default values
dwDefFontHeight=iLineHgt=10;
iFontZoom=1; // default: no internal font zooming - likely no antialiasing either...
iHSpace=-1;
iGfxLineHgt=iLineHgt+1;
dwWeight=FW_NORMAL;
fDoShadow=false;
// font not yet initialized
*szFontName=0;
id=0;
#endif
}
/* Text size measurement */
bool CStdFont::GetTextExtent(const char *szText, int32_t &rsx, int32_t &rsy, bool fCheckMarkup)
{
// safety
if (!szText) return false;
assert(IsValidUtf8(szText));
#ifdef USE_CONSOLE
rsx = rsy = 0;
#else
// keep track of each row's size
int iRowWdt=0,iWdt=0,iHgt=iLineHgt;
// ignore any markup
C4Markup MarkupChecker(false);
// go through all text
while (*szText)
{
// ignore markup
if (fCheckMarkup) MarkupChecker.SkipTags(&szText);
// get current char
uint32_t c = GetNextCharacter(&szText);
// done? (must check here, because markup-skip may have led to text end)
if (!c) break;
// line break?
if (c == '\n' || (fCheckMarkup && c == '|')) { iRowWdt=0; iHgt+=iLineHgt; continue; }
// ignore system characters
if (c < ' ') continue;
// image?
int iImgLgt;
if (fCheckMarkup && c=='{' && szText[0]=='{' && szText[1]!='{' && (iImgLgt=SCharPos('}', szText+1))>0 && szText[iImgLgt+2]=='}')
{
char imgbuf[101];
SCopy(szText+1, imgbuf, Min(iImgLgt, 100));
int w2, h2;
if(!GetFontImageSize(imgbuf, w2, h2))
{ w2 = 0; h2 = 0; }
iRowWdt += w2;
// skip image tag
szText+=iImgLgt+3;
}
else
{
// regular char
// look up character width in texture coordinates table
iRowWdt += GetCharacterFacet(c).Wdt / iFontZoom;
}
// apply horizontal indent for all but last char
if (*szText) iRowWdt += iHSpace;
// adjust max row size
if (iRowWdt>iWdt) iWdt=iRowWdt;
}
// store output
rsx=iWdt; rsy=iHgt;
// done, success
#endif
return true;
}
int CStdFont::BreakMessage(const char *szMsg, int iWdt, char *szOut, int iMaxOutLen, bool fCheckMarkup, float fZoom)
{
#ifdef USE_CONSOLE
return 0;
#else
// 2do: Implement this in terms of StdStrBuf-version
// note iMaxOutLen does not include terminating null character
// safety
if (!iMaxOutLen) return 0;
if (!szMsg)
{
if (szOut) *szOut=0;
return 0;
}
// TODO: might szLastBreakOut, szLastEmergencyBreakPos, szLastEmergencyBreakOut, iXEmergencyBreak not be properly initialised before use?
uint32_t c;
const char *szPos=szMsg, // current parse position in the text
*szLastBreakPos = szMsg, // points to the char after at (whitespace) or after ('-') which text can be broken
*szLastEmergenyBreakPos = NULL, // same, but at last char in case no suitable linebreak could be found
*szLastPos; // last position until which buffer has been transferred to output
char *szLastBreakOut = NULL, *szLastEmergencyBreakOut = NULL; // position of output pointer at break positions
int iX=0, // current text width at parse pos
iXBreak=0, // text width as it was at last break pos
iXEmergencyBreak = 0, // same, but at last char in case no suitable linebreak could be found
iHgt=iLineHgt; // total height of output text
bool fIsFirstLineChar = true;
// ignore any markup
C4Markup MarkupChecker(false);
// go through all text
while (*(szLastPos = szPos))
{
// ignore markup
if (fCheckMarkup) MarkupChecker.SkipTags(&szPos);
// get current char
c = GetNextCharacter(&szPos);
// done? (must check here, because markup-skip may have led to text end)
if (!c) break;
// manual break?
int iCharWdt = 0;
if (c != '\n' && (!fCheckMarkup || c != '|'))
{
// image?
int iImgLgt;
if (fCheckMarkup && c=='{' && szPos[0]=='{' && szPos[1]!='{' && (iImgLgt=SCharPos('}', szPos+1))>0 && szPos[iImgLgt+2]=='}')
{
char imgbuf[101];
SCopy(szPos+1, imgbuf, Min(iImgLgt, 100));
int iCharHgt;
if(!GetFontImageSize(imgbuf, iCharWdt, iCharHgt))
iCharWdt = 0;
// skip image tag
szPos+=iImgLgt+3;
}
else
{
// regular char
// look up character width in texture coordinates table
if (c >= ' ')
iCharWdt = int(fZoom * GetCharacterFacet(c).Wdt / iFontZoom) + iHSpace;
else
iCharWdt = 0; // OMFG ctrl char
}
// add chars to output
while (szLastPos != szPos)
{
if (szOut)
*szOut++ = *szLastPos++;
else
++szLastPos;
if (szOut && !--iMaxOutLen)
{
// buffer end: cut and terminate
*szOut = '\0';
break;
}
}
if (szOut && !iMaxOutLen) break;
// add to line; always add one char at minimum
if ((iX+=iCharWdt) <= iWdt || fIsFirstLineChar)
{
// check whether linebreak possibility shall be marked here
// 2do: What about unicode-spaces?
if (c<256) if (isspace((unsigned char)c) || c == '-')
{
szLastBreakPos = szPos;
szLastBreakOut = szOut;
// space: Break directly at space if it isn't the first char here
// first char spaces must remain, in case the output area is just one char width
if (c != '-' && !fIsFirstLineChar) --szLastBreakPos; // because c<256, the character length can be safely assumed to be 1 here
iXBreak = iX;
}
// always mark emergency break after char that fitted the line
szLastEmergenyBreakPos = szPos;
iXEmergencyBreak = iX;
szLastEmergencyBreakOut = szOut;
// line OK; continue filling it
fIsFirstLineChar = false;
continue;
}
// line must be broken now
// if there was no linebreak, do it at emergency pos
if (szLastBreakPos == szMsg)
{
szLastBreakPos = szLastEmergenyBreakPos;
szLastBreakOut = szLastEmergencyBreakOut;
iXBreak = iXEmergencyBreak;
}
// insert linebreak at linebreak pos
// was it a space? Then just overwrite space with a linebreak
if (uint8_t(*szLastBreakPos)<128 && isspace((unsigned char)*szLastBreakPos))
*(szLastBreakOut-1) = '\n';
else
{
// otherwise, insert line break
if (szOut && !--iMaxOutLen)
// buffer is full
break;
if (szOut)
{
char *szOut2 = szOut;
while (--szOut2 >= szLastBreakOut)
szOut2[1] = *szOut2;
szOut2[1] = '\n';
}
}
// calc next line usage
iX -= iXBreak;
}
else
{
// a static linebreak: Everything's well; this just resets the line width
iX = 0;
}
// forced or manual line break: set new line beginning to char after line break
szLastBreakPos = szMsg = szPos;
// manual line break or line width overflow: add char to next line
iHgt += iLineHgt;
fIsFirstLineChar = true;
}
// transfer final data to buffer - markup and terminator
if (szOut)
while ((*szOut++ = *szLastPos++))
if (!--iMaxOutLen)
{
// buffer end: cut and terminate
*szOut = '\0';
break;
}
// return text height
return iHgt;
#endif
}
int CStdFont::BreakMessage(const char *szMsg, int iWdt, StdStrBuf *pOut, bool fCheckMarkup, float fZoom)
{
#ifdef USE_CONSOLE
return 0;
#else
// safety
if (!szMsg || !pOut) return 0;
pOut->Clear();
// TODO: might szLastEmergenyBreakPos, iLastBreakOutLen or iXEmergencyBreak not be properly initialised before use?
uint32_t c;
const char *szPos=szMsg, // current parse position in the text
*szLastBreakPos = szMsg, // points to the char after at (whitespace) or after ('-') which text can be broken
*szLastEmergenyBreakPos = NULL, // same, but at last char in case no suitable linebreak could be found
*szLastPos; // last position until which buffer has been transferred to output
int iLastBreakOutLen = 0, iLastEmergencyBreakOutLen = 0; // size of output string at break positions
int iX=0, // current text width at parse pos
iXBreak=0, // text width as it was at last break pos
iXEmergencyBreak = 0, // same, but at last char in case no suitable linebreak could be found
iHgt=iLineHgt; // total height of output text
int iCharHOverlap = Max<int>(-iHSpace, 0); // character width exceeding placement of next character
bool fIsFirstLineChar = true;
// ignore any markup
C4Markup MarkupChecker(false);
// go through all text
while (*(szLastPos = szPos))
{
// ignore markup
if (fCheckMarkup) MarkupChecker.SkipTags(&szPos);
// get current char
c = GetNextCharacter(&szPos);
// done? (must check here, because markup-skip may have led to text end)
if (!c) break;
// manual break?
int iCharWdt = 0;
if (c != '\n' && (!fCheckMarkup || c != '|'))
{
// image?
int iImgLgt;
if (fCheckMarkup && c=='{' && szPos[0]=='{' && szPos[1]!='{' && (iImgLgt=SCharPos('}', szPos+1))>0 && szPos[iImgLgt+2]=='}')
{
char imgbuf[101];
SCopy(szPos+1, imgbuf, Min(iImgLgt, 100));
int iCharHgt;
if(!GetFontImageSize(imgbuf, iCharWdt, iCharHgt))
iCharWdt = 0;
// skip image tag
szPos+=iImgLgt+3;
}
else
{
// regular char
// look up character width in texture coordinates table
if (c >= ' ')
iCharWdt = int(fZoom * GetCharacterFacet(c).Wdt / iFontZoom) + iHSpace;
else
iCharWdt = 0; // OMFG ctrl char
}
// add chars to output
pOut->Append(szLastPos, szPos - szLastPos);
// add to line; always add one char at minimum
if ((iX+=iCharWdt)+iCharHOverlap <= iWdt || fIsFirstLineChar)
{
// check whether linebreak possibility shall be marked here
// 2do: What about unicode-spaces?
if (c<256) if (isspace((unsigned char)c) || c == '-')
{
szLastBreakPos = szPos;
iLastBreakOutLen = pOut->getLength();
// space: Break directly at space if it isn't the first char here
// first char spaces must remain, in case the output area is just one char width
if (c != '-' && !fIsFirstLineChar) --szLastBreakPos; // because c<256, the character length can be safely assumed to be 1 here
iXBreak = iX;
}
// always mark emergency break after char that fitted the line
szLastEmergenyBreakPos = szPos;
iXEmergencyBreak = iX;
iLastEmergencyBreakOutLen = pOut->getLength();
// line OK; continue filling it
fIsFirstLineChar = false;
continue;
}
// line must be broken now
// check if a linebreak is possible directly here, because it's a space
// only check for space and not for other breakable characters (such as '-'), because the break would happen after those characters instead of at them
if (c<128 && isspace((unsigned char)c))
{
szLastBreakPos = szPos-1;
iLastBreakOutLen = pOut->getLength();
iXBreak = iX;
}
// if there was no linebreak, do it at emergency pos
else if (szLastBreakPos == szMsg)
{
szLastBreakPos = szLastEmergenyBreakPos;
iLastBreakOutLen = iLastEmergencyBreakOutLen;
iXBreak = iXEmergencyBreak;
}
// insert linebreak at linebreak pos
// was it a space? Then just overwrite space with a linebreak
if (uint8_t(*szLastBreakPos)<128 && isspace((unsigned char)*szLastBreakPos))
*pOut->getMPtr(iLastBreakOutLen-1) = '\n';
else
{
// otherwise, insert line break
pOut->InsertChar('\n', iLastBreakOutLen);
}
// calc next line usage
iX -= iXBreak;
}
else
{
// a static linebreak: Everything's well; this just resets the line width
iX = 0;
// add to output
pOut->Append(szLastPos, szPos - szLastPos);
}
// forced or manual line break: set new line beginning to char after line break
szLastBreakPos = szMsg = szPos;
// manual line break or line width overflow: add char to next line
iHgt += iLineHgt;
fIsFirstLineChar = true;
}
// transfer final data to buffer (any missing markup)
pOut->Append(szLastPos, szPos - szLastPos);
// return text height
return iHgt;
#endif
}
// get message break and pos after message break
// 2do: Function not ready for UTF-8, markup or inline images. Remove its usage using standardized BreakMessage
int CStdFont::GetMessageBreak(const char *szMsg, const char **ppNewPos, int iBreakWidth, float fZoom)
{
#ifdef USE_CONSOLE
*ppNewPos = szMsg;
while(**ppNewPos) ++*ppNewPos;
return *ppNewPos - szMsg;
#else
// safety
if (!szMsg || !*szMsg) { *ppNewPos = szMsg; return 0; }
const char *szPos = szMsg; unsigned char c;
int iWdt = 0; int iPos = 0;
// check all message until it's too wide
while ((c = *szPos++))
{
++iPos;
// get char width
int iCharWdt = int(fZoom * fctAsciiTexCoords[c-' '].Wdt / iFontZoom) + iHSpace;
// add to overall line width
iWdt += iCharWdt;
// next char only if the line didn't overflow
if (iWdt > iBreakWidth) break;
}
// did it all fit?
if (!c)
{
// all OK then; use all the buffer
*ppNewPos = szPos-1;
return iPos;
}
// line must be broken - trace back until first break char
// szPos2 will be first char of next line
const char *szPos2 = szPos-1; int i=0;
while ((!i++ || *szPos2 != '-') && *szPos2 != ' ')
if (szPos2 == szMsg)
{
// do not go past beginning of line
// then better print out an unfitting break
szPos2 = szPos-1;
break;
}
else
--szPos2;
// but do print at least one char
if (szPos2 <= szMsg) szPos2 = szMsg+1;
// assign next line start pos - skip spaces
*ppNewPos = szPos2;
if (*szPos2 == ' ') ++*ppNewPos;
// return output string length
return szPos2 - szMsg;
#endif
}
/* Text drawing */
void CStdFont::DrawText(C4Surface * sfcDest, float iX, float iY, DWORD dwColor, const char *szText, DWORD dwFlags, C4Markup &Markup, float fZoom)
{
#ifndef USE_CONSOLE
assert(IsValidUtf8(szText));
C4DrawTransform bt, *pbt=NULL;
// set blit color
DWORD dwOldModClr;
bool fWasModulated = pDraw->GetBlitModulation(dwOldModClr);
if (fWasModulated) ModulateClr(dwColor, dwOldModClr);
// get alpha fade percentage
DWORD dwAlphaMod = Min<uint32_t>(((dwColor>>0x18)*0xff)/0xaf, 255)<<0x18 | 0xffffff;
/* char TEXT[8192];
sprintf(TEXT, "%s(%x-%x-%x)", szText, dwAlphaMod>>0x18, dwColor>>0x15, (((int)(dwColor>>0x15)-0x50)*0xff)/0xaf); szText=TEXT;*/
// adjust text starting position (horizontal only)
if (dwFlags & STDFONT_CENTERED)
{
// centered
int32_t sx,sy;
GetTextExtent(szText, sx,sy, !(dwFlags & STDFONT_NOMARKUP));
sx = int(fZoom*sx); sy = int(fZoom*sy);
iX-=sx/2;
}
else if (dwFlags & STDFONT_RIGHTALGN)
{
// right-aligned
int32_t sx,sy;
GetTextExtent(szText, sx,sy, !(dwFlags & STDFONT_NOMARKUP));
sx = int(fZoom*sx); sy = int(fZoom*sy);
iX-=sx;
}
// apply texture zoom
fZoom /= iFontZoom;
// set start markup transformation
if (!Markup.Clean()) pbt=&bt;
// output text
uint32_t c;
C4Facet fctFromBlt; // source facet
while ((c = GetNextCharacter(&szText)))
{
// ignore system characters
if (c < ' ') continue;
// apply markup
if (c=='<' && (~dwFlags & STDFONT_NOMARKUP))
{
// get tag
if (Markup.Read(&--szText))
{
// mark transform to be done
// (done only if tag was found, so most normal blits don't init a trasnformation matrix)
pbt=&bt;
// skip the tag
continue;
}
// invalid tag: render it as text
++szText;
}
int w2, h2; // dst width/height
// custom image?
int iImgLgt;
char imgbuf[101] = "";
if (c=='{' && szText[0]=='{' && szText[1]!='{' && (iImgLgt=SCharPos('}', szText+1))>0 && szText[iImgLgt+2]=='}' && !(dwFlags & STDFONT_NOMARKUP))
{
SCopy(szText+1, imgbuf, Min(iImgLgt, 100));
szText+=iImgLgt+3;
if(!GetFontImageSize(imgbuf, w2, h2))
continue;
//normal: not modulated, unless done by transform or alpha fadeout
if ((dwColor>>0x18) >= 0xaf)
pDraw->DeactivateBlitModulation();
else
pDraw->ActivateBlitModulation((dwColor&0xff000000) | 0xffffff);
}
else
{
// regular char
// get texture coordinates
fctFromBlt = GetCharacterFacet(c);
if(!fctFromBlt.Surface) continue;
w2=int(fctFromBlt.Wdt*fZoom); h2=int(fctFromBlt.Hgt*fZoom);
pDraw->ActivateBlitModulation(dwColor);
}
// do color/markup
if (pbt)
{
// reset data to be transformed by markup
DWORD dwBlitClr = dwColor;
bt.Set(1,0,0,0,1,0,0,0,1);
// apply markup
Markup.Apply(bt, dwBlitClr);
if (dwBlitClr != dwColor) ModulateClrA(dwBlitClr, dwAlphaMod);
pDraw->ActivateBlitModulation(dwBlitClr);
// move transformation center to center of letter
float fOffX=(float) w2/2 + iX;
float fOffY=(float) h2/2 + iY;
bt.mat[2] += fOffX - fOffX*bt.mat[0] - fOffY*bt.mat[1];
bt.mat[5] += fOffY - fOffX*bt.mat[3] - fOffY*bt.mat[4];
}
if(imgbuf[0])
{
C4Facet fct;
fct.Set(sfcDest, iX, iY + (iGfxLineHgt - h2)/2.0f, w2, h2);
pCustomImages->DrawFontImage(imgbuf, fct, pbt);
}
else
{
// blit character
pDraw->Blit(fctFromBlt.Surface, float(fctFromBlt.X), float(fctFromBlt.Y), float(fctFromBlt.Wdt),float(fctFromBlt.Hgt),
sfcDest, iX, iY, float(w2), float(h2),
true, pbt);
}
// advance pos and skip character indent
iX+=w2+iHSpace;
}
// reset blit modulation
if (fWasModulated)
pDraw->ActivateBlitModulation(dwOldModClr);
else
pDraw->DeactivateBlitModulation();
#endif
}
bool CStdFont::GetFontImageSize(const char* szTag, int& width, int& height) const
{
#ifdef USE_CONSOLE
width = height = 0;
#else
const float aspect = pCustomImages ? pCustomImages->GetFontImageAspect(szTag) : -1.0f;
// aspect < 0 means there is no such image
if (aspect < 0.0f) return false;
// image found: adjust aspect by font height and calc appropriate width
height = iGfxLineHgt;
width = static_cast<int>(height * aspect + 0.5f);
// make images not ridiciously wide
if(width > height)
{
float scale = static_cast<float>(height)/static_cast<float>(width);
width = height;//static_cast<int32_t>(width*scale + 0.5f);
height = static_cast<int32_t>(height*scale + 0.5f);
}
#endif
return true;
}