1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2003, Sven2
6 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18/* freetype support by JanL and Guenther */
19// text drawing facility for CStdDDraw
20
21#include "C4Config.h"
22#include <Standard.h>
23#include <StdBuf.h>
24#include "StdFont.h"
25#include <StdDDraw2.h>
26#include <C4Surface.h>
27#include <StdMarkup.h>
28
29#include <cmath>
30#include <stdexcept>
31#include <string>
32
33#ifdef _WIN32
34#include <tchar.h>
35#include <stdio.h>
36#else
37#define _T(x) x
38#endif // _WIN32
39
40#ifdef HAVE_FREETYPE
41#include <ft2build.h>
42#include FT_FREETYPE_H
43#endif // HAVE_FREETYPE
44
45#ifdef HAVE_ICONV
46#include <iconv.h>
47#endif // HAVE_ICONV
48
49/* Initialization */
50
51#ifdef HAVE_FREETYPE
52class CStdVectorFont
53{
54 FT_Library library;
55 FT_Face face;
56
57public:
58 CStdVectorFont(const char *filepathname)
59 {
60 // Initialize Freetype
61 if (FT_Init_FreeType(alibrary: &library))
62 throw std::runtime_error("Cannot init Freetype");
63 // Load the font
64 FT_Error e;
65 if (e = FT_New_Face(library, filepathname, face_index: 0, aface: &face))
66 throw std::runtime_error(std::format(fmt: "Cannot load {}: {}", args&: filepathname, args&: e));
67 }
68
69 CStdVectorFont(const StdBuf &Data)
70 {
71 // Initialize Freetype
72 if (FT_Init_FreeType(alibrary: &library))
73 throw std::runtime_error("Cannot init Freetype");
74 // Load the font
75 FT_Error e;
76 if (e = FT_New_Memory_Face(library, file_base: static_cast<const FT_Byte *>(Data.getData()), file_size: Data.getSize(), face_index: 0, aface: &face))
77 throw std::runtime_error(std::format(fmt: "Cannot load font: {}", args&: e));
78 }
79
80 ~CStdVectorFont()
81 {
82 FT_Done_Face(face);
83 FT_Done_FreeType(library);
84 }
85
86 operator FT_Face() { return face; }
87 FT_Face operator->() { return face; }
88};
89
90CStdVectorFont *CStdFont::CreateFont(const char *szFaceName)
91{
92 return new CStdVectorFont(szFaceName);
93}
94
95CStdVectorFont *CStdFont::CreateFont(const StdBuf &Data)
96{
97 return new CStdVectorFont(Data);
98}
99
100void CStdFont::DestroyFont(CStdVectorFont *pFont)
101{
102 delete pFont;
103}
104
105#else
106
107CStdVectorFont *CStdFont::CreateFont(const StdBuf &Data)
108{
109 return 0;
110}
111
112CStdVectorFont *CStdFont::CreateFont(const char *szFaceName)
113{
114 return 0;
115}
116
117void CStdFont::DestroyFont(CStdVectorFont *pFont) {}
118
119#endif
120
121CStdFont::CStdFont()
122{
123 // set default values
124 psfcFontData = nullptr;
125 sfcCurrent = nullptr;
126 iNumFontSfcs = 0;
127 iSfcSizes = 64;
128 dwDefFontHeight = iLineHgt = 10;
129 iFontZoom = 1; // default: no internal font zooming - likely no antialiasing either...
130 iHSpace = -1;
131 iGfxLineHgt = iLineHgt + 1;
132 dwWeight = FW_NORMAL;
133 fDoShadow = false;
134 fUTF8 = false;
135 // font not yet initialized
136 *szFontName = 0;
137 id = 0;
138 pCustomImages = nullptr;
139 fPrerenderedFont = false;
140#ifdef HAVE_FREETYPE
141 pVectorFont = nullptr;
142#endif
143}
144
145bool CStdFont::AddSurface()
146{
147 // add new surface as render target; copy old ones
148 C4Surface **pNewSfcs = new C4Surface *[iNumFontSfcs + 1];
149 if (iNumFontSfcs) memcpy(dest: pNewSfcs, src: psfcFontData, n: iNumFontSfcs * sizeof(C4Surface *));
150 delete[] psfcFontData;
151 psfcFontData = pNewSfcs;
152 C4Surface *sfcNew = psfcFontData[iNumFontSfcs] = new C4Surface();
153 ++iNumFontSfcs;
154 if (iSfcSizes) if (!sfcNew->Create(iWdt: iSfcSizes, iHgt: iSfcSizes)) return false;
155 if (sfcCurrent)
156 {
157 sfcCurrent->Unlock();
158 }
159 sfcCurrent = sfcNew;
160 sfcCurrent->Lock();
161 iCurrentSfcX = iCurrentSfcY = 0;
162 return true;
163}
164
165bool CStdFont::CheckRenderedCharSpace(uint32_t iCharWdt, uint32_t iCharHgt)
166{
167 // need to do a line break?
168 if (iCurrentSfcX + iCharWdt >= static_cast<uint32_t>(iSfcSizes)) if (iCurrentSfcX)
169 {
170 iCurrentSfcX = 0;
171 iCurrentSfcY += iCharHgt;
172 if (iCurrentSfcY + iCharHgt >= static_cast<uint32_t>(iSfcSizes))
173 {
174 // surface is full: Next one
175 if (!AddSurface()) return false;
176 }
177 }
178 // OK draw it there
179 return true;
180}
181
182bool CStdFont::AddRenderedChar(uint32_t dwChar, C4Facet *pfctTarget)
183{
184 int shadowSize = fDoShadow ? static_cast<int>(std::round(x: scale)) : 0;
185
186#ifdef HAVE_FREETYPE
187 // Freetype character rendering
188 FT_Set_Pixel_Sizes(face: *pVectorFont, pixel_width: dwDefFontHeight, pixel_height: dwDefFontHeight);
189 int32_t iBoldness = dwWeight - 400; // zero is normal; 300 is bold
190 if (iBoldness)
191 {
192 iBoldness = (1 << 16) + (iBoldness << 16) / 400;
193 FT_Matrix mat;
194 mat.xx = iBoldness; mat.xy = mat.yx = 0; mat.yy = 1 << 16;
195 // .*(100 + iBoldness/3)/100
196 FT_Set_Transform(face: *pVectorFont, matrix: &mat, delta: nullptr);
197 }
198 else
199 {
200 FT_Set_Transform(face: *pVectorFont, matrix: nullptr, delta: nullptr);
201 }
202 // Render
203 if (FT_Load_Char(face: *pVectorFont, char_code: dwChar, FT_LOAD_RENDER | FT_LOAD_NO_HINTING))
204 {
205 // although the character was not drawn, assume it's not in the font and won't be needed
206 // so return success here
207 return true;
208 }
209 // Make a shortcut to the glyph
210 FT_GlyphSlot slot = (*pVectorFont)->glyph;
211 if (slot->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
212 {
213 // although the character was drawn in a strange way, assume it's not in the font and won't be needed
214 // so return success here
215 return true;
216 }
217 // linebreak/ new surface check
218 int width = std::max<int>(a: slot->advance.x / 64, b: (std::max)(a: slot->bitmap_left, b: 0) + slot->bitmap.width) + shadowSize;
219 if (!CheckRenderedCharSpace(iCharWdt: width, iCharHgt: iGfxLineHgt)) return false;
220 // offset from the top
221 int at_y = iCurrentSfcY + dwDefFontHeight * (*pVectorFont)->ascender / (*pVectorFont)->units_per_EM - slot->bitmap_top;
222 int at_x = iCurrentSfcX + (std::max)(a: slot->bitmap_left, b: 0);
223 // Copy to the surface
224 for (unsigned int y = 0; y < slot->bitmap.rows + shadowSize; ++y)
225 {
226 for (unsigned int x = 0; x < slot->bitmap.width + shadowSize; ++x)
227 {
228 unsigned char bAlpha, bAlphaShadow;
229 if (x < slot->bitmap.width && y < slot->bitmap.rows)
230 bAlpha = 255 - slot->bitmap.buffer[slot->bitmap.width * y + x];
231 else
232 bAlpha = 255;
233 // Make a shadow from the upper-left pixel, and blur with the eight neighbors
234 uint32_t dwPixVal = 0u;
235 bAlphaShadow = 255;
236 if (fDoShadow && x >= shadowSize && y >= shadowSize)
237 {
238 int iShadow = 0;
239 if (x < slot->bitmap.width && y < slot->bitmap.rows) iShadow += slot->bitmap.buffer[(x - (shadowSize - 1)) + slot->bitmap.width * (y - (shadowSize - 1))];
240 if (x > shadowSize && y < slot->bitmap.rows) iShadow += slot->bitmap.buffer[(x - (shadowSize + 1)) + slot->bitmap.width * (y - (shadowSize - 1))];
241 if (x > (shadowSize - 1) && y < slot->bitmap.rows) iShadow += slot->bitmap.buffer[(x - shadowSize) + slot->bitmap.width * (y - (shadowSize - 1))];
242 if (x < slot->bitmap.width && y > shadowSize) iShadow += slot->bitmap.buffer[(x - (shadowSize - 1)) + slot->bitmap.width * (y - (shadowSize + 1))];
243 if (x > shadowSize && y > shadowSize) iShadow += slot->bitmap.buffer[(x - (shadowSize + 1)) + slot->bitmap.width * (y - (shadowSize + 1))];
244 if (x > (shadowSize - 1) && y > shadowSize) iShadow += slot->bitmap.buffer[(x - shadowSize) + slot->bitmap.width * (y - (shadowSize + 1))];
245 if (x < slot->bitmap.width && y > (shadowSize - 1)) iShadow += slot->bitmap.buffer[(x - (shadowSize - 1)) + slot->bitmap.width * (y - shadowSize)];
246 if (x > shadowSize && y > (shadowSize - 1)) iShadow += slot->bitmap.buffer[(x - (shadowSize + 1)) + slot->bitmap.width * (y - shadowSize)];
247 if (x > (shadowSize - 1) && y > (shadowSize - 1)) iShadow += slot->bitmap.buffer[(x - shadowSize) + slot->bitmap.width * (y - shadowSize)] * 8;
248 bAlphaShadow -= iShadow / 16;
249 // because blitting on a black pixel reduces luminosity as compared to shadowless font,
250 // assume luminosity as if blitting shadowless font on a 50% gray background
251 unsigned char cBack = (255 - bAlpha);
252 dwPixVal = RGB(r: cBack / 2, g: cBack / 2, b: cBack / 2);
253 }
254 dwPixVal += bAlphaShadow << 24;
255 BltAlpha(dst&: dwPixVal, src: bAlpha << 24 | 0xffffff);
256 sfcCurrent->SetPixDw(iX: at_x + x, iY: at_y + y, dwCol: dwPixVal);
257 }
258 }
259 // Save the position of the glyph for the rendering code
260 pfctTarget->Set(nsfc: sfcCurrent, nx: iCurrentSfcX, ny: iCurrentSfcY, nwdt: width, nhgt: iGfxLineHgt);
261
262#endif // end of freetype rendering
263
264 // advance texture position
265 iCurrentSfcX += pfctTarget->Wdt;
266 return true;
267}
268
269uint32_t CStdFont::GetNextUTF8Character(const char **pszString)
270{
271 // assume the current character is UTF8 already (i.e., highest bit set)
272 const char *szString = *pszString;
273 unsigned char c = *szString++;
274 uint32_t dwResult = '?';
275 assert(c > 127);
276 if (c > 191 && c < 224)
277 {
278 unsigned char c2 = *szString++;
279 if ((c2 & 192) != 128) { *pszString = szString; return '?'; }
280 dwResult = ((c & 31) << 6) | (c2 & 63); // two char code
281 }
282 else if (c >= 224 && c <= 239)
283 {
284 unsigned char c2 = *szString++;
285 if ((c2 & 192) != 128) { *pszString = szString; return '?'; }
286 unsigned char c3 = *szString++;
287 if ((c3 & 192) != 128) { *pszString = szString; return '?'; }
288 dwResult = ((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63); // three char code
289 }
290 else if (c >= 240 && c <= 247)
291 {
292 unsigned char c2 = *szString++;
293 if ((c2 & 192) != 128) { *pszString = szString; return '?'; }
294 unsigned char c3 = *szString++;
295 if ((c3 & 192) != 128) { *pszString = szString; return '?'; }
296 unsigned char c4 = *szString++;
297 if ((c4 & 192) != 128) { *pszString = szString; return '?'; }
298 dwResult = ((c & 7) << 18) | ((c2 & 63) << 12) | ((c3 & 63) << 6) | (c4 & 63); // four char code
299 }
300 *pszString = szString;
301 return dwResult;
302}
303
304C4Facet &CStdFont::GetUnicodeCharacterFacet(uint32_t c)
305{
306 // find/add facet in map
307 C4Facet &rFacet = fctUnicodeMap[c];
308 // create character on the fly if necessary and possible
309 if (!rFacet.Surface && !fPrerenderedFont)
310 {
311 sfcCurrent->Lock();
312 AddRenderedChar(dwChar: c, pfctTarget: &rFacet);
313 sfcCurrent->Unlock();
314 }
315 // rendering might have failed, in which case rFacet remains empty. Should be OK; char won't be printed then
316 return rFacet;
317}
318
319void CStdFont::Init(CStdVectorFont &VectorFont, uint32_t dwHeight, uint32_t dwFontWeight, const char *szCharset, bool fDoShadow, float scale)
320{
321 const auto realHeight = dwHeight;
322 // clear previous
323 Clear();
324 this->scale = scale;
325 dwHeight = static_cast<uint32_t>(dwHeight * scale);
326 // set values
327 iHSpace = fDoShadow ? -1 : 0; // horizontal shadow
328 dwWeight = dwFontWeight;
329 this->fDoShadow = fDoShadow;
330 if (SEqual(szStr1: szCharset, szStr2: "UTF-8")) fUTF8 = true;
331 // determine needed texture size
332 if (dwHeight * iFontZoom > 40)
333 iSfcSizes = 512;
334 else if (dwDefFontHeight * iFontZoom > 20)
335 iSfcSizes = 256;
336 else
337 iSfcSizes = 128;
338 dwDefFontHeight = dwHeight;
339 // create surface
340 if (!AddSurface())
341 {
342 Clear();
343 throw std::runtime_error(std::string("Cannot create surface (") + szFontName + ")");
344 }
345
346#ifdef HAVE_FREETYPE
347 // Store vector font - assumed to be held externally!
348 pVectorFont = &VectorFont;
349 // Get size
350 // FIXME: use bbox or dynamically determined line heights here
351 iLineHgt = (VectorFont->ascender - VectorFont->descender) * dwHeight / VectorFont->units_per_EM;
352 iGfxLineHgt = iLineHgt + fDoShadow; // vertical shadow
353
354#else
355
356 throw std::runtime_error("You have a engine without Truetype support.");
357
358#endif // HAVE_FREETYPE
359
360 // loop through all ANSI/ASCII printable characters and prepare them
361 // in case of UTF8, unicode characters will be created on the fly and extended ASCII characters (128-255) are not needed
362 // now render all characters!
363
364#if defined(HAVE_ICONV)
365 // Initialize iconv
366 struct iconv_t_wrapper
367 {
368 iconv_t i;
369 iconv_t_wrapper(const char *to, const char *from)
370 {
371 i = iconv_open(tocode: to, fromcode: from);
372 if (i == iconv_t(-1))
373 throw std::runtime_error(std::string("Cannot open iconv (") + to + ", " + from + ")");
374 }
375 ~iconv_t_wrapper() { iconv_close(cd: i); }
376 operator iconv_t() { return i; }
377 };
378#ifdef __BIG_ENDIAN__
379 iconv_t_wrapper iconv_handle("UCS-4BE", C4Config::GetCharsetCodeName(szCharset));
380#else
381 iconv_t_wrapper iconv_handle("UCS-4LE", C4Config::GetCharsetCodeName(charset: szCharset));
382#endif
383#elif defined(_WIN32)
384 int32_t iCodePage = C4Config::GetCharsetCodePage(szCharset);
385#endif
386 int cMax = fUTF8 ? 127 : 255;
387 for (int c = ' '; c <= cMax; ++c)
388 {
389 uint32_t dwChar = c;
390#if defined(HAVE_ICONV)
391 // convert from whatever legacy encoding in use to unicode
392 if (!fUTF8)
393 {
394 // Convert to unicode
395 char chr = dwChar;
396 char *in = &chr;
397 char *out = reinterpret_cast<char *>(&dwChar);
398 size_t insize = 1;
399 size_t outsize = 4;
400 iconv(cd: iconv_handle, inbuf: const_cast<ICONV_CONST char * *>(&in), inbytesleft: &insize, outbuf: &out, outbytesleft: &outsize);
401 }
402#elif defined(_WIN32)
403 // convert using Win32 API
404 if (!fUTF8 && c >= 128)
405 {
406 char cc[2] = { static_cast<char>(c), '\0' };
407 wchar_t outbuf[4];
408 if (MultiByteToWideChar(iCodePage, 0, cc, -1, outbuf, 4)) // 2do: Convert using proper codepage
409 {
410 // now convert from UTF-16 to UCS-4
411 if (((outbuf[0] & 0xfc00) == 0xd800) && ((outbuf[1] & 0xfc00) == 0xdc00))
412 {
413 dwChar = 0x10000 + (((outbuf[0] & 0x3ff) << 10) | (outbuf[1] & 0x3ff));
414 }
415 else
416 dwChar = outbuf[0];
417 }
418 else
419 {
420 // conversion error. Shouldn't be fatal; just pretend it were a Unicode character
421 }
422 }
423#else
424 // no conversion available? Just break for non-iso8859-1.
425#endif // defined HAVE_ICONV
426 if (!AddRenderedChar(dwChar, pfctTarget: &(fctAsciiTexCoords[c - ' '])))
427 {
428 sfcCurrent->Unlock();
429 Clear();
430 throw std::runtime_error(std::string("Cannot render characters for Font (") + szFontName + ")");
431 }
432 }
433
434 sfcCurrent->Unlock();
435
436 // adjust line height
437 iLineHgt /= iFontZoom;
438 this->scale = static_cast<float>(dwHeight) / realHeight;
439
440 fPrerenderedFont = false;
441 if (0) for (int i = 0; i < iNumFontSfcs; ++i)
442 {
443 const std::string pngFilename{std::format(fmt: "{}{}{}_{}.png", args: +szFontName, args&: dwHeight, args: fDoShadow ? "_shadow" : "", args&: i)};
444 psfcFontData[i]->SavePNG(szFilename: pngFilename.c_str(), fSaveAlpha: true, fApplyGamma: false, fSaveOverlayOnly: false);
445 }
446}
447
448const uint32_t FontDelimeterColor = 0xff0000,
449 FontDelimiterColorLB = 0x00ff00,
450 FontDelimeterColorIndent1 = 0xffff00,
451 FontDelimeterColorIndent2 = 0xff00ff;
452
453// perform color matching in 16 bit
454inline bool ColorMatch(uint32_t dwClr1, uint32_t dwClr2)
455{
456 return ClrDw2W(dwClr: dwClr1) == ClrDw2W(dwClr: dwClr2);
457}
458
459void CStdFont::Init(const char *szFontName, C4Surface *psfcFontSfc, int iIndent)
460{
461 // clear previous
462 Clear();
463 // grab surface
464 iSfcSizes = 0;
465 if (!AddSurface()) { Clear(); throw std::runtime_error(std::string("Error creating surface for ") + szFontName); }
466 *sfcCurrent = std::move(*psfcFontSfc);
467 // extract character positions from image data
468 if (!sfcCurrent->Hgt)
469 {
470 Clear();
471 throw std::runtime_error(std::string("Error loading ") + szFontName);
472 }
473 // get line height
474 iGfxLineHgt = 1;
475 while (iGfxLineHgt < sfcCurrent->Hgt)
476 {
477 uint32_t dwPix = sfcCurrent->GetPixDw(iX: 0, iY: iGfxLineHgt, fApplyModulation: false);
478 if (ColorMatch(dwClr1: dwPix, dwClr2: FontDelimeterColor) || ColorMatch(dwClr1: dwPix, dwClr2: FontDelimiterColorLB) ||
479 ColorMatch(dwClr1: dwPix, dwClr2: FontDelimeterColorIndent1) || ColorMatch(dwClr1: dwPix, dwClr2: FontDelimeterColorIndent2))
480 break;
481 ++iGfxLineHgt;
482 }
483 // set font height and width indent
484 dwDefFontHeight = iLineHgt = iGfxLineHgt - iIndent;
485 iHSpace = -iIndent;
486 // determine character sizes
487 int iX = 0, iY = 0;
488 for (int c = ' '; c < 256; ++c)
489 {
490 // save character pos
491 fctAsciiTexCoords[c - ' '].X = iX; // left
492 fctAsciiTexCoords[c - ' '].Y = iY; // top
493 bool IsLB = false;
494 // get horizontal extent
495 while (iX < sfcCurrent->Wdt)
496 {
497 uint32_t dwPix = sfcCurrent->GetPixDw(iX, iY, fApplyModulation: false);
498 if (ColorMatch(dwClr1: dwPix, dwClr2: FontDelimeterColor) || ColorMatch(dwClr1: dwPix, dwClr2: FontDelimeterColorIndent1) || ColorMatch(dwClr1: dwPix, dwClr2: FontDelimeterColorIndent2))
499 break;
500 if (ColorMatch(dwClr1: dwPix, dwClr2: FontDelimiterColorLB)) { IsLB = true; break; }
501 ++iX;
502 }
503 // remove vertical line
504 if (iX < sfcCurrent->Wdt)
505 for (int y = 0; y < iGfxLineHgt; ++y)
506 sfcCurrent->SetPixDw(iX, iY: iY + y, dwCol: 0xffffffff);
507 // save char size
508 fctAsciiTexCoords[c - ' '].Wdt = iX - fctAsciiTexCoords[c - ' '].X;
509 fctAsciiTexCoords[c - ' '].Hgt = iGfxLineHgt;
510 // next line?
511 if (++iX >= sfcCurrent->Wdt || IsLB)
512 {
513 iY += iGfxLineHgt;
514 iX = 0;
515 // remove horizontal line
516 if (iY < sfcCurrent->Hgt)
517 for (int x = 0; x < sfcCurrent->Wdt; ++x)
518 sfcCurrent->SetPixDw(iX: x, iY, dwCol: 0xffffffff);
519 // skip empty line
520 ++iY;
521 // end reached?
522 if (iY + iGfxLineHgt > sfcCurrent->Hgt)
523 {
524 // all filled
525 break;
526 }
527 }
528 }
529 // release texture data
530 sfcCurrent->Unlock();
531 // adjust line height
532 iLineHgt /= iFontZoom;
533 // set name
534 SCopy(szSource: szFontName, sTarget: this->szFontName);
535 // mark prerendered
536 fPrerenderedFont = true;
537}
538
539void CStdFont::Clear()
540{
541#ifdef HAVE_FREETYPE
542 pVectorFont = nullptr;
543#endif
544 // clear font sfcs
545 if (psfcFontData)
546 {
547 while (iNumFontSfcs--) delete psfcFontData[iNumFontSfcs];
548 delete[] psfcFontData;
549 psfcFontData = nullptr;
550 }
551 sfcCurrent = nullptr;
552 iNumFontSfcs = 0;
553 for (int c = ' '; c < 256; ++c) fctAsciiTexCoords[c - ' '].Default();
554 fctUnicodeMap.clear();
555 // set default values
556 dwDefFontHeight = iLineHgt = 10;
557 iFontZoom = 1; // default: no internal font zooming - likely no antialiasing either...
558 iHSpace = -1;
559 iGfxLineHgt = iLineHgt + 1;
560 dwWeight = FW_NORMAL;
561 fDoShadow = false;
562 fPrerenderedFont = false;
563 fUTF8 = false;
564 // font not yet initialized
565 *szFontName = 0;
566 id = 0;
567}
568
569/* Text size measurement */
570
571bool CStdFont::GetTextExtent(const char *szText, int32_t &rsx, int32_t &rsy, bool fCheckMarkup, bool ignoreScale)
572{
573 float realScale = 1.f;
574 if (!ignoreScale)
575 {
576 realScale = scale;
577 }
578 // safety
579 if (!szText) return false;
580 // keep track of each row's size
581 int lineStepHeight = static_cast<int>(std::ceil(x: iLineHgt / realScale));
582 float iRowWdt = 0, iWdt = 0;
583 int iHgt = lineStepHeight;
584 // ignore any markup
585 CMarkup MarkupChecker(false);
586 // go through all text
587 while (*szText)
588 {
589 // ignore markup
590 if (fCheckMarkup) MarkupChecker.SkipTags(ppText: &szText);
591 // get current char
592 uint32_t c = GetNextCharacter(pszString: &szText);
593 // done? (must check here, because markup-skip may have led to text end)
594 if (!c) break;
595 // line break?
596 if (c == _T('\n') || (fCheckMarkup && c == _T('|'))) { iRowWdt = 0; iHgt += lineStepHeight; continue; }
597 // ignore system characters
598 if (c < _T(' ')) continue;
599 // image?
600 int iImgLgt;
601 if (fCheckMarkup && c == '{' && szText[0] == '{' && szText[1] != '{' && (iImgLgt = SCharPos(cTarget: '}', szInStr: szText + 1)) > 0 && szText[iImgLgt + 2] == '}')
602 {
603 char imgbuf[101];
604 SCopy(szSource: szText + 1, sTarget: imgbuf, iMaxL: (std::min)(a: iImgLgt, b: 100));
605 C4Facet fct;
606 // image renderer initialized?
607 if (pCustomImages)
608 // try to get an image then
609 pCustomImages->GetFontImage(szImageTag: imgbuf, rOutImgFacet&: fct);
610 if (fct.Hgt)
611 {
612 // image found: adjust aspect by font height and calc appropriate width
613 iRowWdt += (fct.Wdt * iGfxLineHgt) / realScale / fct.Hgt;
614 }
615 else
616 {
617 // image renderer not hooked or ID not found, or surface not present: just ignore it
618 // printing it out wouldn't look better...
619 }
620 // skip image tag
621 szText += iImgLgt + 3;
622 }
623 else
624 {
625 // regular char
626 // look up character width in texture coordinates table
627 iRowWdt += GetCharacterFacet(c).Wdt / realScale / iFontZoom;
628 }
629 // apply horizontal indent for all but last char
630 if (*szText) iRowWdt += iHSpace;
631 // adjust max row size
632 if (iRowWdt > iWdt) iWdt = iRowWdt;
633 }
634 // store output
635 rsx = static_cast<int>(iWdt); rsy = iHgt;
636 // done, success
637 return true;
638}
639
640int CStdFont::BreakMessage(const char *szMsg, int iWdt, StdStrBuf *pOut, bool fCheckMarkup, float fZoom, size_t maxLines)
641{
642 // safety
643 if (!szMsg || !pOut) return 0;
644 pOut->Clear();
645 uint32_t c;
646 const char *szPos = szMsg, // current parse position in the text
647 *szLastBreakPos = szMsg, // points to the char after at (whitespace) or after ('-') which text can be broken
648 *szLastEmergenyBreakPos, // same, but at last char in case no suitable linebreak could be found
649 *szLastPos; // last position until which buffer has been transferred to output
650 int iLastBreakOutLen, iLastEmergencyBreakOutLen; // size of output string at break positions
651 float iX = 0, // current text width at parse pos
652 iXBreak = 0, // text width as it was at last break pos
653 iXEmergencyBreak; // same, but at last char in case no suitable linebreak could be found
654 int iHgt = GetLineHeight(); // total height of output text
655 bool fIsFirstLineChar = true;
656 // ignore any markup
657 CMarkup MarkupChecker(false);
658 // go through all text
659 while (*(szLastPos = szPos))
660 {
661 // ignore markup
662 if (fCheckMarkup) MarkupChecker.SkipTags(ppText: &szPos);
663 // get current char
664 c = GetNextCharacter(pszString: &szPos);
665 // done? (must check here, because markup-skip may have led to text end)
666 if (!c) break;
667 // manual break?
668 float iCharWdt = 0;
669 if (c != '\n' && (!fCheckMarkup || c != '|'))
670 {
671 // image?
672 int iImgLgt;
673 if (fCheckMarkup && c == '{' && szPos[0] == '{' && szPos[1] != '{' && (iImgLgt = SCharPos(cTarget: '}', szInStr: szPos + 1)) > 0 && szPos[iImgLgt + 2] == '}')
674 {
675 char imgbuf[101];
676 SCopy(szSource: szPos + 1, sTarget: imgbuf, iMaxL: (std::min)(a: iImgLgt, b: 100));
677 C4Facet fct;
678 // image renderer initialized?
679 if (pCustomImages)
680 // try to get an image then
681 pCustomImages->GetFontImage(szImageTag: imgbuf, rOutImgFacet&: fct);
682 if (fct.Hgt)
683 {
684 // image found: adjust aspect by font height and calc appropriate width
685 iCharWdt = (fct.Wdt * iGfxLineHgt) / scale / fct.Hgt;
686 }
687 else
688 {
689 // image renderer not hooked or ID not found, or surface not present: just ignore it
690 // printing it out wouldn't look better...
691 iCharWdt = 0;
692 }
693 // skip image tag
694 szPos += iImgLgt + 3;
695 }
696 else
697 {
698 // regular char
699 // look up character width in texture coordinates table
700 if (c >= ' ')
701 iCharWdt = fZoom * GetCharacterFacet(c).Wdt / iFontZoom / scale + iHSpace;
702 else
703 iCharWdt = 0; // OMFG ctrl char
704 }
705 // add chars to output
706 pOut->Append(pnData: szLastPos, iChars: szPos - szLastPos);
707 // add to line; always add one char at minimum
708 if ((iX += iCharWdt) <= iWdt || fIsFirstLineChar)
709 {
710 // check whether linebreak possibility shall be marked here
711 // 2do: What about unicode-spaces?
712 if (c < 256) if (isspace(static_cast<unsigned char>(c)) || c == '-')
713 {
714 szLastBreakPos = szPos;
715 iLastBreakOutLen = pOut->getLength();
716 // space: Break directly at space if it isn't the first char here
717 // first char spaces must remain, in case the output area is just one char width
718 if (c != '-' && !fIsFirstLineChar) --szLastBreakPos; // because c<256, the character length can be safely assumed to be 1 here
719 iXBreak = iX;
720 }
721 // always mark emergency break after char that fitted the line
722 szLastEmergenyBreakPos = szPos;
723 iXEmergencyBreak = iX;
724 iLastEmergencyBreakOutLen = pOut->getLength();
725 // line OK; continue filling it
726 fIsFirstLineChar = false;
727 continue;
728 }
729 // line must be broken now
730 // check if a linebreak is possible directly here, because it's a space
731 // only check for space and not for other breakable characters (such as '-'), because the break would happen after those characters instead of at them
732 if (c < 128 && isspace(static_cast<unsigned char>(c)))
733 {
734 szLastBreakPos = szPos - 1;
735 iLastBreakOutLen = pOut->getLength();
736 iXBreak = iX;
737 }
738 // if there was no linebreak, do it at emergency pos
739 else if (szLastBreakPos == szMsg)
740 {
741 szLastBreakPos = szLastEmergenyBreakPos;
742 iLastBreakOutLen = iLastEmergencyBreakOutLen;
743 iXBreak = iXEmergencyBreak;
744 }
745 StdStrBuf tempPart;
746 // insert linebreak at linebreak pos
747 // was it a space? Then just overwrite space with a linebreak
748 if (static_cast<uint8_t>(*szLastBreakPos) < 128 && isspace(static_cast<unsigned char>(*szLastBreakPos)))
749 {
750 *pOut->getMPtr(i: iLastBreakOutLen - 1) = '\n';
751 if (fCheckMarkup)
752 {
753 tempPart.Copy(pnData: pOut->getMPtr(i: iLastBreakOutLen));
754 pOut->SetLength(iLastBreakOutLen - 1);
755 }
756 }
757 else
758 {
759 // otherwise, insert line break
760 pOut->InsertChar(cChar: '\n', insert_before: iLastBreakOutLen);
761 if (fCheckMarkup)
762 {
763 tempPart.Copy(pnData: pOut->getMPtr(i: iLastBreakOutLen) + 1);
764 pOut->SetLength(iLastBreakOutLen);
765 }
766 }
767 if (fCheckMarkup)
768 {
769 CMarkup markup(false);
770 const char *data = pOut->getData();
771 const char *lastLine = (std::max)(a: data + pOut->getSize() - 3, b: data);
772 while (lastLine > data && *lastLine != '\n') --lastLine;
773 while (*lastLine)
774 {
775 while (*lastLine == '<' && markup.Read(ppText: &lastLine));
776 if (*lastLine) ++lastLine;
777 }
778 pOut->Append(pnData: markup.ToCloseMarkup().c_str());
779 pOut->AppendChar(cChar: '\n');
780 pOut->Append(pnData: markup.ToMarkup().c_str());
781 pOut->Append(Buf2: tempPart);
782 }
783 // calc next line usage
784 iX -= iXBreak;
785 }
786 else
787 {
788 // a static linebreak: Everything's well; this just resets the line width
789 iX = 0;
790 // add to output
791 pOut->Append(pnData: szLastPos, iChars: szPos - szLastPos);
792 }
793 // forced or manual line break: set new line beginning to char after line break
794 szLastBreakPos = szMsg = szPos;
795 // manual line break or line width overflow: add char to next line
796 iHgt += GetLineHeight();
797 fIsFirstLineChar = true;
798
799 if (maxLines == 1)
800 {
801 pOut->Append(pnData: szPos);
802 return iHgt;
803 }
804 else if (maxLines != 0) --maxLines;
805 }
806 // transfer final data to buffer (any missing markup)
807 pOut->Append(pnData: szLastPos, iChars: szPos - szLastPos);
808 // return text height
809 return iHgt;
810}
811
812/* Text drawing */
813
814void CStdFont::DrawText(C4Surface *sfcDest, int iX, int iY, uint32_t dwColor, const char *szText, uint32_t dwFlags, CMarkup &Markup, float fZoom)
815{
816 float x = static_cast<float>(iX), y = static_cast<float>(iY);
817 CBltTransform bt, *pbt = nullptr;
818 // set blit color
819 dwColor = InvertRGBAAlpha(dwFromClr: dwColor);
820 uint32_t dwOldModClr;
821 bool fWasModulated = lpDDraw->GetBlitModulation(rdwColor&: dwOldModClr);
822 if (fWasModulated) ModulateClr(dst&: dwColor, src: dwOldModClr);
823 // get alpha fade percentage
824 uint32_t dwAlphaMod = BoundBy<int>(bval: ((static_cast<int>(dwColor >> 0x18) - 0x50) * 0xff) / 0xaf, lbound: 0, rbound: 255) << 0x18 | 0xffffff;
825 // adjust text starting position (horizontal only)
826 if (dwFlags & STDFONT_CENTERED)
827 {
828 // centered
829 int32_t sx, sy;
830 GetTextExtent(szText, rsx&: sx, rsy&: sy, fCheckMarkup: !(dwFlags & STDFONT_NOMARKUP));
831 x -= fZoom * (sx / 2);
832 }
833 else if (dwFlags & STDFONT_RIGHTALGN)
834 {
835 // right-aligned
836 int32_t sx, sy;
837 GetTextExtent(szText, rsx&: sx, rsy&: sy, fCheckMarkup: !(dwFlags & STDFONT_NOMARKUP));
838 x -= fZoom * sx;
839 }
840 // apply texture zoom
841 fZoom /= scale;
842 fZoom /= iFontZoom;
843 // set start markup transformation
844 if (!Markup.Clean()) pbt = &bt;
845 // output text
846 uint32_t c;
847 C4Facet fctFromBlt; // source facet
848 while (c = GetNextCharacter(pszString: &szText))
849 {
850 // ignore system characters
851 if (c < _T(' ')) continue;
852 // apply markup
853 if (c == '<' && (~dwFlags & STDFONT_NOMARKUP))
854 {
855 // get tag
856 if (Markup.Read(ppText: &--szText))
857 {
858 // mark transform to be done
859 // (done only if tag was found, so most normal blits don't init a trasnformation matrix)
860 pbt = &bt;
861 // skip the tag
862 continue;
863 }
864 // invalid tag: render it as text
865 ++szText;
866 }
867 float w2, h2; // dst width/height
868 // custom image?
869 int iImgLgt;
870 if (c == '{' && szText[0] == '{' && szText[1] != '{' && (iImgLgt = SCharPos(cTarget: '}', szInStr: szText + 1)) > 0 && szText[iImgLgt + 2] == '}' && !(dwFlags & STDFONT_NOMARKUP))
871 {
872 fctFromBlt.Default();
873 char imgbuf[101];
874 SCopy(szSource: szText + 1, sTarget: imgbuf, iMaxL: (std::min)(a: iImgLgt, b: 100));
875 szText += iImgLgt + 3;
876 // image renderer initialized?
877 if (pCustomImages)
878 // try to get an image then
879 pCustomImages->GetFontImage(szImageTag: imgbuf, rOutImgFacet&: fctFromBlt);
880 if (fctFromBlt.Surface && fctFromBlt.Hgt)
881 {
882 // image found: adjust aspect by font height and calc appropriate width
883 w2 = (fctFromBlt.Wdt * iGfxLineHgt) * fZoom / fctFromBlt.Hgt;
884 h2 = iGfxLineHgt * fZoom;
885 }
886 else
887 {
888 // image renderer not hooked or ID not found, or surface not present: just ignore it
889 // printing it out wouldn't look better...
890 continue;
891 }
892 // normal: not modulated, unless done by transform or alpha fadeout
893 if ((dwColor >> 0x18) <= 0x50)
894 lpDDraw->DeactivateBlitModulation();
895 else
896 lpDDraw->ActivateBlitModulation(dwWithClr: (dwColor & 0xff000000) | 0xffffff);
897 }
898 else
899 {
900 // regular char
901 // get texture coordinates
902 fctFromBlt = GetCharacterFacet(c);
903 w2 = fctFromBlt.Wdt * fZoom; h2 = fctFromBlt.Hgt * fZoom;
904 lpDDraw->ActivateBlitModulation(dwWithClr: dwColor);
905 }
906 // do color/markup
907 if (pbt)
908 {
909 // reset data to be transformed by markup
910 uint32_t dwBlitClr = dwColor;
911 bt.Set(fA: 1, fB: 0, fC: 0, fD: 0, fE: 1, fF: 0, fG: 0, fH: 0, fI: 1);
912 // apply markup
913 Markup.Apply(rBltTrf&: bt, dwClr&: dwBlitClr);
914 if (dwBlitClr != dwColor) ModulateClrA(dst&: dwBlitClr, src: dwAlphaMod);
915 lpDDraw->ActivateBlitModulation(dwWithClr: dwBlitClr);
916 // move transformation center to center of letter
917 float fOffX = w2 / 2 + x;
918 float fOffY = h2 / 2 + y;
919 bt.mat[2] += fOffX - fOffX * bt.mat[0] - fOffY * bt.mat[1];
920 bt.mat[5] += fOffY - fOffX * bt.mat[3] - fOffY * bt.mat[4];
921 }
922 // blit character or image
923 lpDDraw->Blit(sfcSource: fctFromBlt.Surface, fx: float(fctFromBlt.X), fy: float(fctFromBlt.Y), fwdt: float(fctFromBlt.Wdt), fhgt: float(fctFromBlt.Hgt),
924 sfcTarget: sfcDest, tx: x, ty: y, twdt: w2, thgt: h2,
925 fSrcColKey: true, pTransform: pbt, noScalingCorrection: true);
926 // advance pos and skip character indent
927 x += w2 + iHSpace;
928 }
929 // reset blit modulation
930 if (fWasModulated)
931 lpDDraw->ActivateBlitModulation(dwWithClr: dwOldModClr);
932 else
933 lpDDraw->DeactivateBlitModulation();
934}
935
936int CStdFont::GetLineHeight() const
937{
938 return static_cast<int>(iLineHgt / scale);
939}
940