1 /*
2  *  The ManaPlus Client
3  *  Copyright (C) 2004-2009  The Mana World Development Team
4  *  Copyright (C) 2009-2010  The Mana Developers
5  *  Copyright (C) 2009  Aethyra Development Team
6  *  Copyright (C) 2011-2019  The ManaPlus Developers
7  *  Copyright (C) 2019-2021  Andrei Karas
8  *
9  *  This file is part of The ManaPlus Client.
10  *
11  *  This program is free software; you can redistribute it and/or modify
12  *  it under the terms of the GNU General Public License as published by
13  *  the Free Software Foundation; either version 2 of the License, or
14  *  any later version.
15  *
16  *  This program is distributed in the hope that it will be useful,
17  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  *  GNU General Public License for more details.
20  *
21  *  You should have received a copy of the GNU General Public License
22  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
23  */
24 
25 /*      _______   __   __   __   ______   __   __   _______   __   __
26  *     / _____/\ / /\ / /\ / /\ / ____/\ / /\ / /\ / ___  /\ /  |\/ /\
27  *    / /\____\// / // / // / // /\___\// /_// / // /\_/ / // , |/ / /
28  *   / / /__   / / // / // / // / /    / ___  / // ___  / // /| ' / /
29  *  / /_// /\ / /_// / // / // /_/_   / / // / // /\_/ / // / |  / /
30  * /______/ //______/ //_/ //_____/\ /_/ //_/ //_/ //_/ //_/ /|_/ /
31  * \______\/ \______\/ \_\/ \_____\/ \_\/ \_\/ \_\/ \_\/ \_\/ \_\/
32  *
33  * Copyright (c) 2004 - 2008 Olof Naessén and Per Larsson
34  *
35  *
36  * Per Larsson a.k.a finalman
37  * Olof Naessén a.k.a jansem/yakslem
38  *
39  * Visit: http://guichan.sourceforge.net
40  *
41  * License: (BSD)
42  * Redistribution and use in source and binary forms, with or without
43  * modification, are permitted provided that the following conditions
44  * are met:
45  * 1. Redistributions of source code must retain the above copyright
46  *    notice, this list of conditions and the following disclaimer.
47  * 2. Redistributions in binary form must reproduce the above copyright
48  *    notice, this list of conditions and the following disclaimer in
49  *    the documentation and/or other materials provided with the
50  *    distribution.
51  * 3. Neither the name of Guichan nor the names of its contributors may
52  *    be used to endorse or promote products derived from this software
53  *    without specific prior written permission.
54  *
55  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
56  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
57  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
58  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
59  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
60  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
61  * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
62  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
63  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
64  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
65  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
66  */
67 
68 #include "gui/fonts/font.h"
69 
70 #include "fs/files.h"
71 #include "fs/paths.h"
72 
73 #include "fs/virtfs/tools.h"
74 #ifdef USE_SDL2
75 #include "fs/virtfs/rwops.h"
76 #endif  // USE_SDL2
77 
78 #include "gui/fonts/textchunk.h"
79 
80 #include "render/graphics.h"
81 
82 #include "resources/imagehelper.h"
83 
84 #include "resources/image/image.h"
85 
86 #include "utils/checkutils.h"
87 #include "utils/delete2.h"
88 #include "utils/sdlcheckutils.h"
89 #include "utils/stringutils.h"
90 #include "utils/timer.h"
91 
92 #include "debug.h"
93 
94 const unsigned int CACHE_SIZE = 256;
95 const unsigned int CACHE_SIZE_SMALL1 = 2;
96 const unsigned int CACHE_SIZE_SMALL2 = 50;
97 const unsigned int CACHE_SIZE_SMALL3 = 170;
98 const unsigned int CLEAN_TIME = 7;
99 
100 bool Font::mSoftMode(false);
101 
102 extern char *restrict strBuf;
103 
104 static int fontCounter;
105 
Font(std::string filename,int size,const int style)106 Font::Font(std::string filename,
107            int size,
108            const int style) :
109     mFont(nullptr),
110     mCreateCounter(0),
111     mDeleteCounter(0),
112     mCleanTime(cur_time + CLEAN_TIME)
113 {
114     if (fontCounter == 0)
115     {
116         mSoftMode = imageHelper->useOpenGL() == RENDER_SOFTWARE;
117         if (TTF_Init() == -1)
118         {
119             logger->error("Unable to initialize SDL_ttf: " +
120                 std::string(SDL_GetError()));
121         }
122     }
123 
124     if (size < 4)
125     {
126         reportAlways("Error: requested load font %s with size %d",
127             filename.c_str(),
128             size)
129         size = 4;
130     }
131 
132     if (fontCounter == 0)
133     {
134         strBuf = new char[65535];
135         memset(strBuf, 0, 65535);
136     }
137 
138     ++fontCounter;
139 
140     fixDirSeparators(filename);
141     logger->log("Attempt to load font: %s",
142         filename.c_str());
143     mFont = openFont(filename.c_str(), size);
144 
145     if (mFont == nullptr)
146     {
147         logger->log("Error normal loading font " + filename);
148 
149         filename = "fonts/dejavusans.ttf";
150         mFont = openFont(fixDirSeparators(filename).c_str(), size);
151         if (mFont == nullptr)
152         {
153 #ifdef UNITTESTS
154             reportAlways("Font load failed %s",
155                 filename.c_str())
156 #endif  // UNITTESTS
157             logger->error("Font::Font: " +
158                 std::string(SDL_GetError()));
159         }
160         else
161         {
162             logger->log("Loaded fallback font %s, %d",
163                 filename.c_str(),
164                 size);
165         }
166     }
167     else
168     {
169         logger->log("Loaded font %s, %d",
170             filename.c_str(),
171             size);
172     }
173 
174     TTF_SetFontStyle(mFont, style);
175 }
176 
~Font()177 Font::~Font()
178 {
179     TTF_CloseFont(mFont);
180     mFont = nullptr;
181     --fontCounter;
182     clear();
183 
184     if (fontCounter == 0)
185     {
186         TTF_Quit();
187         delete []strBuf;
188     }
189 }
190 
openFont(const char * const name,const int size)191 TTF_Font *Font::openFont(const char *const name,
192                          const int size)
193 {
194 #ifdef USE_SDL2
195     SDL_RWops *const rw = VirtFs::rwopsOpenRead(name);
196     if (rw)
197     {
198         logger->log("Loading virtfs font file: %s",
199             name);
200         return TTF_OpenFontIndexRW(rw, 1, size, 0);
201     }
202 #endif
203     const std::string path = VirtFs::getPath(name);
204     if (Files::existsLocal(path) == false)
205     {
206 #ifndef UNITTESTS
207         // +++ in future need trigger assert in unit tests here too
208         reportAlways("Font::openFont font not exists: %s",
209             path.c_str())
210 #endif  // UNITTESTS
211         return nullptr;
212     }
213     logger->log("Loading physical font file: %s",
214         path.c_str());
215     return TTF_OpenFontIndex(path.c_str(),
216         size, 0);
217 }
218 
loadFont(std::string filename,const int size,const int style)219 void Font::loadFont(std::string filename,
220                     const int size,
221                     const int style)
222 {
223     if (fontCounter == 0 && TTF_Init() == -1)
224     {
225         logger->log("Unable to initialize SDL_ttf: " +
226                     std::string(SDL_GetError()));
227         return;
228     }
229 
230     fixDirSeparators(filename);
231     TTF_Font *const font = openFont(filename.c_str(), size);
232 
233     if (font == nullptr)
234     {
235         logger->log("Font::Font: " +
236                     std::string(SDL_GetError()));
237         return;
238     }
239 
240     if (mFont != nullptr)
241         TTF_CloseFont(mFont);
242 
243     mFont = font;
244     TTF_SetFontStyle(mFont, style);
245     clear();
246 }
247 
clear()248 void Font::clear()
249 {
250     for (size_t f = 0; f < CACHES_NUMBER; f ++)
251         mCache[f].clear();
252 }
253 
drawString(Graphics * const graphics,Color col,const Color & col2,const std::string & text,const int x,const int y)254 void Font::drawString(Graphics *const graphics,
255                       Color col,
256                       const Color &col2,
257                       const std::string &text,
258                       const int x, const int y)
259 {
260     BLOCK_START("Font::drawString")
261     if (text.empty())
262     {
263         BLOCK_END("Font::drawString")
264         return;
265     }
266 
267 //    Color col = graphics->getColor();
268 //    const Color &col2 = graphics->getColor2();
269     const float alpha = static_cast<float>(col.a) / 255.0F;
270 
271     /* The alpha value is ignored at string generation so avoid caching the
272      * same text with different alpha values.
273      */
274     col.a = 255;
275 
276     const unsigned char chr = text[0];
277     TextChunkList *const cache = &mCache[chr];
278 
279     std::map<TextChunkSmall, TextChunk*> &search = cache->search;
280     std::map<TextChunkSmall, TextChunk*>::iterator i
281         = search.find(TextChunkSmall(text, col, col2));
282     if (i != search.end())
283     {
284         TextChunk *const chunk2 = (*i).second;
285         cache->moveToFirst(chunk2);
286         Image *const image = chunk2->img;
287         if (image != nullptr)
288         {
289             image->setAlpha(alpha);
290             graphics->drawImage(image, x, y);
291         }
292     }
293     else
294     {
295         if (cache->size >= CACHE_SIZE)
296         {
297 #ifdef DEBUG_FONT_COUNTERS
298             mDeleteCounter ++;
299 #endif  // DEBUG_FONT_COUNTERS
300 
301             cache->removeBack();
302         }
303 #ifdef DEBUG_FONT_COUNTERS
304         mCreateCounter ++;
305 #endif  // DEBUG_FONT_COUNTERS
306 
307         TextChunk *chunk2 = new TextChunk(text, col, col2, this);
308 
309         chunk2->generate(mFont, alpha);
310         cache->insertFirst(chunk2);
311 
312         const Image *const image = chunk2->img;
313         if (image != nullptr)
314             graphics->drawImage(image, x, y);
315     }
316     BLOCK_END("Font::drawString")
317 }
318 
slowLogic(const int rnd)319 void Font::slowLogic(const int rnd)
320 {
321     BLOCK_START("Font::slowLogic")
322     if (mCleanTime == 0)
323     {
324         mCleanTime = cur_time + CLEAN_TIME + rnd;
325     }
326     else if (mCleanTime < cur_time)
327     {
328         doClean();
329         mCleanTime = cur_time + CLEAN_TIME + rnd;
330     }
331     BLOCK_END("Font::slowLogic")
332 }
333 
getWidth(const std::string & text) const334 int Font::getWidth(const std::string &text) const
335 {
336     if (text.empty())
337         return 0;
338 
339     const unsigned char chr = text[0];
340     TextChunkList *const cache = &mCache[chr];
341 
342     std::map<std::string, TextChunk*> &search = cache->searchWidth;
343     std::map<std::string, TextChunk*>::iterator i = search.find(text);
344     if (i != search.end())
345     {
346         TextChunk *const chunk = (*i).second;
347         cache->moveToFirst(chunk);
348         const Image *const image = chunk->img;
349         if (image != nullptr)
350             return image->getWidth();
351         return 0;
352     }
353 
354     // if string was not drawed
355     int w;
356     int h;
357     getSafeUtf8String(text, strBuf);
358     TTF_SizeUTF8(mFont, strBuf, &w, &h);
359     return w;
360 }
361 
getHeight() const362 int Font::getHeight() const
363 {
364     return TTF_FontHeight(mFont);
365 }
366 
doClean()367 void Font::doClean()
368 {
369     for (unsigned int f = 0; f < CACHES_NUMBER; f ++)
370     {
371         TextChunkList *const cache = &mCache[f];
372         const size_t size = CAST_SIZE(cache->size);
373 #ifdef DEBUG_FONT_COUNTERS
374         logger->log("ptr: %u, size: %ld", f, size);
375 #endif  // DEBUG_FONT_COUNTERS
376 
377         if (size > CACHE_SIZE_SMALL3)
378         {
379 #ifdef DEBUG_FONT_COUNTERS
380             mDeleteCounter += 100;
381 #endif  // DEBUG_FONT_COUNTERS
382 
383             cache->removeBack(100);
384 #ifdef DEBUG_FONT_COUNTERS
385             logger->log("delete3");
386 #endif  // DEBUG_FONT_COUNTERS
387         }
388         else if (size > CACHE_SIZE_SMALL2)
389         {
390 #ifdef DEBUG_FONT_COUNTERS
391             mDeleteCounter += 20;
392 #endif  // DEBUG_FONT_COUNTERS
393 
394             cache->removeBack(20);
395 #ifdef DEBUG_FONT_COUNTERS
396             logger->log("delete2");
397 #endif  // DEBUG_FONT_COUNTERS
398         }
399         else if (size > CACHE_SIZE_SMALL1)
400         {
401 #ifdef DEBUG_FONT_COUNTERS
402             mDeleteCounter ++;
403 #endif  // DEBUG_FONT_COUNTERS
404 
405             cache->removeBack();
406 #ifdef DEBUG_FONT_COUNTERS
407             logger->log("delete1");
408 #endif  // DEBUG_FONT_COUNTERS
409         }
410     }
411 }
412 
getStringIndexAt(const std::string & text,const int x) const413 int Font::getStringIndexAt(const std::string& text, const int x) const
414 {
415     const size_t sz = text.size();
416     for (size_t i = 0; i < sz; ++i)
417     {
418         if (getWidth(text.substr(0, i)) > x)
419             return CAST_S32(i);
420     }
421 
422     return CAST_S32(sz);
423 }
424 
getCache() const425 const TextChunkList *Font::getCache() const noexcept2
426 {
427     return mCache;
428 }
429 
generate(TextChunk & chunk)430 void Font::generate(TextChunk &chunk)
431 {
432     const std::string &text = chunk.text;
433     if (text.empty())
434         return;
435 
436     const unsigned char chr = text[0];
437     TextChunkList *const cache = &mCache[chr];
438     Color &col = chunk.color;
439     Color &col2 = chunk.color2;
440     const int oldAlpha = col.a;
441     col.a = 255;
442 
443     TextChunkSmall key(text, col, col2);
444     std::map<TextChunkSmall, TextChunk*> &search = cache->search;
445     std::map<TextChunkSmall, TextChunk*>::iterator i = search.find(key);
446     if (i != search.end())
447     {
448         TextChunk *const chunk2 = (*i).second;
449         cache->moveToFirst(chunk2);
450 //        search.erase(key);
451         cache->remove(chunk2);
452         chunk.img = chunk2->img;
453         chunk2->img = nullptr;
454         delete chunk2;
455 //        logger->log("cached image: " + chunk.text);
456     }
457     else
458     {
459         if (cache->size >= CACHE_SIZE)
460         {
461 #ifdef DEBUG_FONT_COUNTERS
462             mDeleteCounter ++;
463 #endif  // DEBUG_FONT_COUNTERS
464 
465             cache->removeBack();
466         }
467 #ifdef DEBUG_FONT_COUNTERS
468         mCreateCounter ++;
469 #endif  // DEBUG_FONT_COUNTERS
470 
471         const float alpha = static_cast<float>(chunk.color.a) / 255.0F;
472         chunk.generate(mFont, alpha);
473 //        logger->log("generate image: " + chunk.text);
474     }
475     col.a = oldAlpha;
476 }
477 
insertChunk(TextChunk * const chunk)478 void Font::insertChunk(TextChunk *const chunk)
479 {
480     if ((chunk == nullptr) || chunk->text.empty() || (chunk->img == nullptr))
481         return;
482 //    logger->log("insert chunk: text=%s, color: %d,%d,%d",
483 //        chunk->text.c_str(), chunk->color.r, chunk->color.g, chunk->color.b);
484     const unsigned char chr = chunk->text[0];
485     TextChunkList *const cache = &mCache[chr];
486 
487     std::map<TextChunkSmall, TextChunk*> &search = cache->search;
488     std::map<TextChunkSmall, TextChunk*>::iterator i
489         = search.find(TextChunkSmall(chunk->text,
490         chunk->color, chunk->color2));
491     if (i != search.end())
492     {
493         delete2(chunk->img)
494         return;
495     }
496 
497     TextChunk *const chunk2 = new TextChunk(chunk->text,
498         chunk->color, chunk->color2, chunk->textFont);
499     chunk2->img = chunk->img;
500     cache->insertFirst(chunk2);
501 }
502