1/*
2 * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22
23#include "atlas.h"
24#include "bitmapfont.h"
25#include "texturemanager.h"
26#include "graphics.h"
27#include "image.h"
28
29#include <framework/core/eventdispatcher.h>
30#include <framework/otml/otml.h>
31#include <framework/util/extras.h>
32
33void BitmapFont::load(const OTMLNodePtr& fontNode)
34{
35 OTMLNodePtr textureNode = fontNode->at("texture");
36 std::string textureFile = stdext::resolve_path(textureNode->value(), textureNode->source());
37 Size glyphSize = fontNode->valueAt<Size>("glyph-size");
38 m_glyphHeight = fontNode->valueAt<int>("height");
39 m_yOffset = fontNode->valueAt("y-offset", 0);
40 m_firstGlyph = fontNode->valueAt("first-glyph", 32);
41 m_glyphSpacing = fontNode->valueAt("spacing", Size(0,0));
42 int spaceWidth = fontNode->valueAt("space-width", glyphSize.width());
43
44 if(OTMLNodePtr node = fontNode->get("fixed-glyph-width")) {
45 for(int glyph = m_firstGlyph; glyph < 256; ++glyph)
46 m_glyphsSize[glyph] = Size(node->value<int>(), m_glyphHeight);
47 } else {
48 calculateGlyphsWidthsAutomatically(Image::load(textureFile), glyphSize);
49 }
50
51 // 32 is space
52 m_glyphsSize[32].setWidth(spaceWidth);
53
54 // use 127 as spacer [Width: 1], Important for the current NPC highlighting system
55 m_glyphsSize[127].setWidth(1);
56
57 // new line actually has a size that will be useful in multiline algorithm
58 m_glyphsSize[(uchar)'\n'] = Size(1, m_glyphHeight);
59
60 // read custom widths
61 /*
62 if(OTMLNodePtr node = fontNode->get("glyph-widths")) {
63 for(const OTMLNodePtr& child : node->children())
64 m_glyphsSize[stdext::safe_cast<int>(child->tag())].setWidth(child->value<int>());
65 }
66 */
67
68 // load font texture
69 m_texture = g_textures.getTexture(textureFile);
70 if (!m_texture)
71 return;
72 m_texture->update();
73
74 int numHorizontalGlyphs = m_texture->getSize().width() / glyphSize.width();
75#ifdef DONT_CACHE_FONTS
76 Point offset(0, 0);
77#else
78 Point offset = g_atlas.cacheFont(m_texture);
79 m_texture = g_atlas.get(1);
80#endif
81 for (int glyph = m_firstGlyph; glyph < 256; ++glyph) {
82 m_glyphsTextureCoords[glyph].setRect(((glyph - m_firstGlyph) % numHorizontalGlyphs) * glyphSize.width() + offset.x,
83 ((glyph - m_firstGlyph) / numHorizontalGlyphs) * glyphSize.height() + offset.y,
84 m_glyphsSize[glyph].width(),
85 m_glyphHeight);
86 }
87}
88
89void BitmapFont::drawText(const std::string& text, const Point& startPos, const Color& color, bool shadow)
90{
91 Size boxSize = g_painter->getResolution() - startPos.toSize();
92 Rect screenCoords(startPos, boxSize);
93 drawText(text, screenCoords, Fw::AlignTopLeft, shadow);
94}
95
96void BitmapFont::drawText(const std::string& text, const Rect& screenCoords, Fw::AlignmentFlag align, const Color& color, bool shadow)
97{
98 g_drawQueue->addText(static_self_cast<BitmapFont>(), text, screenCoords, align, color, shadow);
99}
100
101void BitmapFont::drawColoredText(const std::string& text, const Rect& screenCoords, Fw::AlignmentFlag align, const std::vector<std::pair<int, Color>>& colors, bool shadow)
102{
103 g_drawQueue->addColoredText(static_self_cast<BitmapFont>(), text, screenCoords, align, colors, shadow);
104}
105
106void BitmapFont::calculateDrawTextCoords(CoordsBuffer& coordsBuffer, const std::string& text, const Rect& screenCoords, Fw::AlignmentFlag align)
107{
108 // prevent glitches from invalid rects
109 if (!screenCoords.isValid() || !m_texture)
110 return;
111
112 int textLenght = text.length();
113
114 // map glyphs positions
115 Size textBoxSize;
116 const std::vector<Point>& glyphsPositions = calculateGlyphsPositions(text, align, &textBoxSize);
117
118 for (int i = 0; i < textLenght; ++i) {
119 int glyph = (uchar)text[i];
120
121 // skip invalid glyphs
122 if (glyph < 32)
123 continue;
124
125 // calculate initial glyph rect and texture coords
126 Rect glyphScreenCoords(glyphsPositions[i], m_glyphsSize[glyph]);
127 Rect glyphTextureCoords = m_glyphsTextureCoords[glyph];
128
129 // first translate to align position
130 if (align & Fw::AlignBottom) {
131 glyphScreenCoords.translate(0, screenCoords.height() - textBoxSize.height());
132 } else if (align & Fw::AlignVerticalCenter) {
133 glyphScreenCoords.translate(0, (screenCoords.height() - textBoxSize.height()) / 2);
134 } else { // AlignTop
135 // nothing to do
136 }
137
138 if (align & Fw::AlignRight) {
139 glyphScreenCoords.translate(screenCoords.width() - textBoxSize.width(), 0);
140 } else if (align & Fw::AlignHorizontalCenter) {
141 glyphScreenCoords.translate((screenCoords.width() - textBoxSize.width()) / 2, 0);
142 } else { // AlignLeft
143 // nothing to do
144 }
145
146 // only render glyphs that are after 0, 0
147 if (glyphScreenCoords.bottom() < 0 || glyphScreenCoords.right() < 0)
148 continue;
149
150 // bound glyph topLeft to 0,0 if needed
151 if (glyphScreenCoords.top() < 0) {
152 glyphTextureCoords.setTop(glyphTextureCoords.top() - glyphScreenCoords.top());
153 glyphScreenCoords.setTop(0);
154 }
155 if (glyphScreenCoords.left() < 0) {
156 glyphTextureCoords.setLeft(glyphTextureCoords.left() - glyphScreenCoords.left());
157 glyphScreenCoords.setLeft(0);
158 }
159
160 // translate rect to screen coords
161 glyphScreenCoords.translate(screenCoords.topLeft());
162
163 // only render if glyph rect is visible on screenCoords
164 if (!screenCoords.intersects(glyphScreenCoords))
165 continue;
166
167 // bound glyph bottomRight to screenCoords bottomRight
168 if (glyphScreenCoords.bottom() > screenCoords.bottom()) {
169 glyphTextureCoords.setBottom(glyphTextureCoords.bottom() + (screenCoords.bottom() - glyphScreenCoords.bottom()));
170 glyphScreenCoords.setBottom(screenCoords.bottom());
171 }
172 if (glyphScreenCoords.right() > screenCoords.right()) {
173 glyphTextureCoords.setRight(glyphTextureCoords.right() + (screenCoords.right() - glyphScreenCoords.right()));
174 glyphScreenCoords.setRight(screenCoords.right());
175 }
176
177 // render glyph
178 coordsBuffer.addRect(glyphScreenCoords, glyphTextureCoords);
179 }
180}
181
182const std::vector<Point>& BitmapFont::calculateGlyphsPositions(const std::string& text,
183 Fw::AlignmentFlag align,
184 Size *textBoxSize)
185{
186 // for performance reasons we use statics vectors that are allocated on demand
187 static thread_local std::vector<Point> glyphsPositions(1);
188 static thread_local std::vector<int> lineWidths(1);
189
190 int textLength = text.length();
191 int maxLineWidth = 0;
192 int lines = 0;
193 int glyph;
194 int i;
195
196 // return if there is no text
197 if(textLength == 0) {
198 if(textBoxSize)
199 textBoxSize->resize(0,m_glyphHeight);
200 return glyphsPositions;
201 }
202
203 // resize glyphsPositions vector when needed
204 if(textLength > (int)glyphsPositions.size())
205 glyphsPositions.resize(textLength);
206
207 // calculate lines width
208 if((align & Fw::AlignRight || align & Fw::AlignHorizontalCenter) || textBoxSize) {
209 lineWidths[0] = 0;
210 for(i = 0; i< textLength; ++i) {
211 glyph = (uchar)text[i];
212
213 if(glyph == (uchar)'\n') {
214 lines++;
215 if(lines+1 > (int)lineWidths.size())
216 lineWidths.resize(lines+1);
217 lineWidths[lines] = 0;
218 } else if(glyph >= 32) {
219 lineWidths[lines] += m_glyphsSize[glyph].width() ;
220 if((i+1 != textLength && text[i+1] != '\n')) // only add space if letter is not the last or before a \n.
221 lineWidths[lines] += m_glyphSpacing.width();
222 maxLineWidth = std::max<int>(maxLineWidth, lineWidths[lines]);
223 }
224 }
225 }
226
227 Point virtualPos(0, m_yOffset);
228 lines = 0;
229 for(i = 0; i < textLength; ++i) {
230 glyph = (uchar)text[i];
231
232 // new line or first glyph
233 if(glyph == (uchar)'\n' || i == 0) {
234 if(glyph == (uchar)'\n') {
235 virtualPos.y += m_glyphHeight + m_glyphSpacing.height();
236 lines++;
237 }
238
239 // calculate start x pos
240 if(align & Fw::AlignRight) {
241 virtualPos.x = (maxLineWidth - lineWidths[lines]);
242 } else if(align & Fw::AlignHorizontalCenter) {
243 virtualPos.x = (maxLineWidth - lineWidths[lines]) / 2;
244 } else { // AlignLeft
245 virtualPos.x = 0;
246 }
247 }
248
249 // store current glyph topLeft
250 glyphsPositions[i] = virtualPos;
251
252 // render only if the glyph is valid
253 if(glyph >= 32 && glyph != (uchar)'\n') {
254 virtualPos.x += m_glyphsSize[glyph].width() + m_glyphSpacing.width();
255 }
256 }
257
258 if(textBoxSize) {
259 textBoxSize->setWidth(maxLineWidth);
260 textBoxSize->setHeight(virtualPos.y + m_glyphHeight);
261 }
262
263 return glyphsPositions;
264}
265
266Size BitmapFont::calculateTextRectSize(const std::string& text)
267{
268 Size size;
269 calculateGlyphsPositions(text, Fw::AlignTopLeft, &size);
270 return size;
271}
272
273std::string BitmapFont::wrapText(const std::string& text, int maxWidth, std::vector<std::pair<int, Color>>* colors)
274{
275 std::string outText;
276 outText.reserve(text.size() * 2); // string append optimization
277
278 int lastSeparator = 0, lastColorSeparator = 0, lineLength = 0, wordLength = 0;
279 for (size_t i = 0, c = 0; i < text.size(); ++i) {
280 uchar glyph = (uchar)text[i];
281 if (text[i] == '\n' || text[i] == ' ') {
282 lineLength += wordLength;
283 if (lineLength > maxWidth) { // too long line with this word
284 if (text[lastSeparator] == ' ') {
285 c -= 1;
286 updateColors(colors, lastColorSeparator, -1);
287 lastSeparator += 1;
288 lastColorSeparator += 1;
289 }
290 outText += '\n';
291 lineLength = wordLength;
292 }
293 for (size_t j = lastSeparator; j < i; ++j) { // copy word
294 outText += text[j];
295 }
296 if (text[i] == '\n') { // if new line was added reset line length
297 outText += '\n';
298 wordLength = 0;
299 lineLength = 0;
300 lastSeparator = i + 1;
301 lastColorSeparator = c;
302 } else { // space
303 wordLength = m_glyphsSize[glyph].width() + m_glyphSpacing.width(); // space
304 lastSeparator = i;
305 lastColorSeparator = c;
306 c += 1;
307 }
308 continue;
309 }
310
311 if (glyph < 32) // invalid character
312 continue;
313
314 wordLength += m_glyphsSize[glyph].width() + m_glyphSpacing.width();
315 if (wordLength > maxWidth) { // too long word, split it
316 if (lineLength != 0) { // add new line if current one is not empty
317 outText += '\n';
318 }
319 if (text[lastSeparator] == ' ') { // ignore space if it's first character in new line
320 c -= 1;
321 lastSeparator += 1;
322 lastColorSeparator += 1;
323 }
324 for (size_t j = lastSeparator; j < i; ++j) { // copy word
325 outText += text[j];
326 }
327 updateColors(colors, lastColorSeparator, 1);
328 outText += '-'; // word continuation
329 outText += '\n'; // new line
330
331 wordLength = m_glyphsSize[glyph].width() + m_glyphSpacing.width();
332 lineLength = 0;
333 lastSeparator = i;
334 lastColorSeparator = c;
335 }
336 c += 1;
337 }
338
339 lineLength += wordLength;
340 if (lineLength > maxWidth) { // too long line with this word
341 updateColors(colors, lastColorSeparator, 1);
342 outText += '\n';
343 lineLength = wordLength;
344 }
345 for (size_t j = lastSeparator; j < text.size(); ++j) { // copy word
346 outText += text[j];
347 }
348 return outText;
349}
350
351void BitmapFont::calculateGlyphsWidthsAutomatically(const ImagePtr& image, const Size& glyphSize)
352{
353 if (!image)
354 return;
355
356 int numHorizontalGlyphs = image->getSize().width() / glyphSize.width();
357 auto texturePixels = image->getPixels();
358
359 // small AI to auto calculate pixels widths
360 for (int glyph = m_firstGlyph; glyph < 256; ++glyph) {
361 Rect glyphCoords(((glyph - m_firstGlyph) % numHorizontalGlyphs) * glyphSize.width(),
362 ((glyph - m_firstGlyph) / numHorizontalGlyphs) * glyphSize.height(),
363 glyphSize.width(),
364 m_glyphHeight);
365 int width = glyphSize.width();
366 for (int x = glyphCoords.left(); x <= glyphCoords.right(); ++x) {
367 int filledPixels = 0;
368 // check if all vertical pixels are alpha
369 for (int y = glyphCoords.top(); y <= glyphCoords.bottom(); ++y) {
370 if (texturePixels[(y * image->getSize().width() * 4) + (x * 4) + 3] != 0)
371 filledPixels++;
372 }
373 if (filledPixels > 0)
374 width = x - glyphCoords.left() + 1;
375 }
376 // store glyph size
377 m_glyphsSize[glyph].resize(width, m_glyphHeight);
378 }
379}
380
381void BitmapFont::updateColors(std::vector<std::pair<int, Color>>* colors, int pos, int newTextLen)
382{
383 if (!colors) return;
384 for (auto& it : *colors) {
385 if (it.first > pos) {
386 it.first += newTextLen;
387 }
388 }
389}
390