FifeGUI 0.3.0
A C++ GUI library designed for games.
imagefont.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
2// SPDX-FileCopyrightText: 2004 - 2008 Olof Naessén and Per Larsson
3// SPDX-FileCopyrightText: 2013 - 2026 Fifengine contributors
4
5// Corresponding header include
6#include "fifechan/imagefont.hpp"
7
8// Standard library includes
9#include <algorithm>
10#include <iostream>
11#include <map>
12#include <numeric>
13#include <sstream>
14#include <string>
15#include <utility>
16#include <vector>
17
18// Platform config include
19#include "fifechan/platform.hpp"
20
21// Project headers (subdirs before local)
22#include "fifechan/color.hpp"
23#include "fifechan/exception.hpp"
24#include "fifechan/graphics.hpp"
25#include "fifechan/image.hpp"
26#include "fifechan/rectangle.hpp"
27
28namespace fcn
29{
30 namespace
31 {
35 Color getBorderDominantColor(Image* img)
36 {
37 int const w = img->getWidth();
38 int const h = img->getHeight();
39 if (w <= 0 || h <= 0) {
40 return Color{0, 0, 0, 255};
41 }
42
43 struct RGB
44 {
45 uint8_t r, g, b;
46 bool operator<(RGB const & o) const
47 {
48 if (r != o.r) {
49 return r < o.r;
50 }
51 if (g != o.g) {
52 return g < o.g;
53 }
54 return b < o.b;
55 }
56 };
57 std::map<RGB, int> freq;
58
59 auto count = [&](int x, int y) {
60 Color const c = img->getPixel(x, y);
61 ++freq[{.r = c.r, .g = c.g, .b = c.b}];
62 };
63
64 for (int x = 0; x < w; ++x) {
65 count(x, 0);
66 count(x, h - 1);
67 }
68 for (int y = 1; y < h - 1; ++y) {
69 count(0, y);
70 count(w - 1, y);
71 }
72
73 RGB best = {.r = 0, .g = 0, .b = 0};
74 int maxCount = 0;
75 for (auto const & p : freq) {
76 if (p.second > maxCount) {
77 maxCount = p.second;
78 best = p.first;
79 }
80 }
81 return Color{best.r, best.g, best.b, static_cast<uint8_t>(255)};
82 }
83
84 Color resolveSeparator(Image* img, ImageFontConfig const & cfg)
85 {
86 switch (cfg.strategy) {
87 case SeparatorStrategy::ExplicitColor:
88 return cfg.explicitSeparator;
89 case SeparatorStrategy::BorderDominant:
90 return getBorderDominantColor(img);
91 case SeparatorStrategy::PixelAtOrigin:
92 case SeparatorStrategy::Auto:
93 default:
94 return img->getPixel(0, 0);
95 }
96 }
97
98 bool isSeparator(Color const & p, Color const & sep)
99 {
100 return p.r == sep.r && p.g == sep.g && p.b == sep.b;
101 }
102
103 std::vector<Rectangle> scanGlyphs(
104 Image* img, int expectedCount, Color const & sep, int padding, bool /*verbose*/)
105 {
106 int const w = img->getWidth();
107 int const h = img->getHeight();
108 std::vector<Rectangle> found;
109
110 int startY = 0;
111 bool foundStart = false;
112 for (int y = 0; y < h && !foundStart; ++y) {
113 for (int x = 0; x < w; ++x) {
114 if (!isSeparator(img->getPixel(x, y), sep)) {
115 startY = y;
116 foundStart = true;
117 break;
118 }
119 }
120 }
121 if (!foundStart) {
122 throwException("Image contains no glyph content");
123 }
124
125 int ycur = startY;
126 int maxRowHeight = 0;
127
128 while (ycur < h && std::cmp_less(found.size(), expectedCount)) {
129 int rowEnd = ycur;
130 for (; rowEnd < h; ++rowEnd) {
131 bool hasContent = false;
132 for (int x = 0; x < w; ++x) {
133 if (!isSeparator(img->getPixel(x, rowEnd), sep)) {
134 hasContent = true;
135 break;
136 }
137 }
138 if (!hasContent) {
139 break;
140 }
141 }
142 if (rowEnd == ycur) {
143 break;
144 }
145
146 int const rowH = rowEnd - ycur;
147 maxRowHeight = std::max(maxRowHeight, rowH);
148
149 auto isSepCol = [&](int col) {
150 // Check top and bottom of column (matches fixedfont.bmp and rpgfont.png)
151 return isSeparator(img->getPixel(col, ycur), sep) &&
152 isSeparator(img->getPixel(col, rowEnd - 1), sep);
153 };
154
155 int xcur = 0;
156 while (xcur < w && std::cmp_less(found.size(), expectedCount)) {
157 while (xcur < w && isSepCol(xcur)) {
158 ++xcur;
159 }
160 if (xcur >= w) {
161 break;
162 }
163
164 int const sx = xcur;
165 while (xcur < w && !isSepCol(xcur)) {
166 ++xcur;
167 }
168
169 int const width = std::max(1, xcur - sx - (padding * 2));
170 int const height = std::max(1, rowH - (padding * 2));
171 found.emplace_back(sx + padding, ycur + padding, width, height);
172 }
173 ycur = rowEnd + 1;
174 }
175 return found;
176 }
177
178 } // anonymous namespace
179
180 ImageFont::ImageFont(std::string const & filename, std::string const & glyphs, ImageFontConfig const & config) :
181 mFilename(filename), mImage(Image::load(filename, false))
182 {
183 if (mImage == nullptr) {
184 throwException(std::string("Failed to load image: ") + filename);
185 }
186
187 int const expected = static_cast<int>(glyphs.size());
188 Color sep = resolveSeparator(mImage, config);
189
190 auto found = scanGlyphs(mImage, expected, sep, config.glyphPadding, config.verbose);
191
192 if (config.strategy == SeparatorStrategy::Auto && static_cast<int>(found.size()) < expected * 0.8) {
193 sep = getBorderDominantColor(mImage);
194 found = scanGlyphs(mImage, expected, sep, config.glyphPadding, config.verbose);
195 }
196
197 if (std::cmp_less(found.size(), expected)) {
198 std::ostringstream os;
199 os << "Image " << mFilename << " is corrupt or uses wrong separator.\n"
200 << "Expected: " << expected << " glyphs, Found: " << found.size() << "\n"
201 << "Detected separator: R:" << static_cast<int>(sep.r) << " G:" << static_cast<int>(sep.g)
202 << " B:" << static_cast<int>(sep.b) << "\n"
203 << "Suggestion: Use ExplicitColor strategy with magenta (255,0,255)";
204 throwException(os.str());
205 }
206
207 for (size_t i = 0; i < glyphs.size(); ++i) {
208 mGlyph.at(static_cast<unsigned char>(glyphs.at(i))) = found.at(i);
209 }
210
211 if (config.verbose) {
212 std::cerr << "[ImageFont] Loaded '" << mFilename << "' ExpectedGlyphs=" << expected
213 << " Found=" << found.size() << " Separator=R:" << static_cast<int>(sep.r)
214 << " G:" << static_cast<int>(sep.g) << " B:" << static_cast<int>(sep.b) << "\n";
215 for (char const glyph : glyphs) {
216 unsigned char const c = static_cast<unsigned char>(glyph);
217 Rectangle const & r = mGlyph.at(c);
218 std::cerr << " glyph '" << glyph << "' (" << static_cast<int>(c) << ") -> x=" << r.x << " y=" << r.y
219 << " w=" << r.width << " h=" << r.height << "\n";
220 }
221 }
222
223 mHeight = std::accumulate(found.begin(), found.end(), 0, [](int maxHeight, auto const & r) {
224 return std::max(maxHeight, r.height);
225 });
226
227 mImage->convertToDisplayFormat();
228 mRowSpacing = 0;
229 mGlyphSpacing = 0;
230 }
231
232 ImageFont::ImageFont(std::string const & filename, std::string const & glyphs) :
233 mFilename(filename), mImage(Image::load(filename, false))
234 {
235
236 Color const separator = mImage->getPixel(0, 0);
237
238 // Find the starting point for glyphs in the image
239 int startColumn = 0;
240 for (; startColumn < mImage->getWidth(); ++startColumn) {
241 if (separator != mImage->getPixel(startColumn, 0)) {
242 break;
243 }
244 }
245
246 // Check for corrupt image (all pixels are separator color)
247 if (startColumn >= mImage->getWidth()) {
248 throwException("Corrupt image.");
249 }
250
251 // Find the height of glyphs
252 int height = 0;
253 for (int j = 0; j < mImage->getHeight(); ++j) {
254 if (separator == mImage->getPixel(startColumn, j)) {
255 break;
256 }
257 ++height;
258 }
259
260 mHeight = height;
261
262 int x = 0;
263 int y = 0;
264
265 // Scan for all glyphs
266 for (char const glyph : glyphs) {
267 auto const k = static_cast<unsigned char>(glyph);
268 mGlyph.at(k) = scanForGlyph(k, x, y, separator);
269 // Update x and y with new coordinates.
270 x = mGlyph.at(k).x + mGlyph.at(k).width;
271 y = mGlyph.at(k).y;
272 }
273
274 mImage->convertToDisplayFormat();
275
276 mRowSpacing = 0;
277 mGlyphSpacing = 0;
278 }
279
280 ImageFont::ImageFont(Image* image, std::string const & glyphs) : mFilename("Image*")
281 {
282
283 if (image == nullptr) {
284 throwException("Font image is nullptr.");
285 }
286 mImage = image;
287
288 Color const separator = mImage->getPixel(0, 0);
289
290 int i = 0;
291 for (i = 0; i < mImage->getWidth() && separator == mImage->getPixel(i, 0); ++i) {
292 }
293
294 if (i >= mImage->getWidth()) {
295 throwException("Corrupt image.");
296 }
297
298 int j = 0;
299 for (j = 0; j < mImage->getHeight(); ++j) {
300 if (separator == mImage->getPixel(i, j)) {
301 break;
302 }
303 }
304
305 mHeight = j;
306 int x = 0;
307 int y = 0;
308
309 for (i = 0; std::cmp_less(i, glyphs.size()); ++i) {
310 unsigned char const glyph = glyphs.at(i);
311
312 mGlyph.at(glyph) = scanForGlyph(glyph, x, y, separator);
313 // Update x and y with new coordinates.
314 x = mGlyph.at(glyph).x + mGlyph.at(glyph).width;
315 y = mGlyph.at(glyph).y;
316 }
317
318 mImage->convertToDisplayFormat();
319
320 mRowSpacing = 0;
321 mGlyphSpacing = 0;
322 }
323
324 ImageFont::ImageFont(Image* image, std::string const & glyphs, ImageFontConfig const & config) : mFilename("Image*")
325 {
326 if (image == nullptr) {
327 throwException("Font image is nullptr.");
328 }
329 mImage = image;
330
331 int const expected = static_cast<int>(glyphs.size());
332 Color sep = resolveSeparator(mImage, config);
333
334 auto found = scanGlyphs(mImage, expected, sep, config.glyphPadding, config.verbose);
335
336 if (config.strategy == SeparatorStrategy::Auto && static_cast<int>(found.size()) < expected * 0.8) {
337 sep = getBorderDominantColor(mImage);
338 found = scanGlyphs(mImage, expected, sep, config.glyphPadding, config.verbose);
339 }
340
341 if (std::cmp_less(found.size(), expected)) {
342 std::ostringstream os;
343 os << "Image " << mFilename << " is corrupt or uses wrong separator.\n"
344 << "Expected: " << expected << " glyphs, Found: " << found.size() << "\n"
345 << "Detected separator: R:" << static_cast<int>(sep.r) << " G:" << static_cast<int>(sep.g)
346 << " B:" << static_cast<int>(sep.b) << "\n"
347 << "Suggestion: Use ExplicitColor strategy with magenta (255,0,255)";
348 throwException(os.str());
349 }
350
351 for (size_t i = 0; i < glyphs.size(); ++i) {
352 mGlyph.at(static_cast<unsigned char>(glyphs.at(i))) = found.at(i);
353 }
354
355 mHeight = std::accumulate(found.begin(), found.end(), 0, [](int maxHeight, auto const & r) {
356 return std::max(maxHeight, r.height);
357 });
358
359 mImage->convertToDisplayFormat();
360 mRowSpacing = 0;
361 mGlyphSpacing = 0;
362 }
363
364 ImageFont::ImageFont(std::string const & filename, unsigned char glyphsFrom, unsigned char glyphsTo) :
365 mFilename(filename), mImage(Image::load(filename, false))
366 {
367
368 Color const separator = mImage->getPixel(0, 0);
369
370 int i = 0;
371 for (i = 0; separator == mImage->getPixel(i, 0) && i < mImage->getWidth(); ++i) {
372 }
373
374 if (i >= mImage->getWidth()) {
375 throwException("Corrupt image.");
376 }
377
378 int j = 0;
379 for (j = 0; j < mImage->getHeight(); ++j) {
380 if (separator == mImage->getPixel(i, j)) {
381 break;
382 }
383 }
384
385 mHeight = j;
386 int x = 0;
387 int y = 0;
388
389 for (i = glyphsFrom; i < glyphsTo + 1; i++) {
390 mGlyph.at(i) = scanForGlyph(i, x, y, separator);
391 // Update x och y with new coordinates.
392 x = mGlyph.at(i).x + mGlyph.at(i).width;
393 y = mGlyph.at(i).y;
394 }
395
396 mImage->convertToDisplayFormat();
397
398 mRowSpacing = 0;
399 mGlyphSpacing = 0;
400 }
401
403 std::string const & filename,
404 unsigned char glyphsFrom,
405 unsigned char glyphsTo,
406 ImageFontConfig const & config) :
407 mFilename(filename), mImage(Image::load(filename, false))
408 {
409 if (mImage == nullptr) {
410 throwException(std::string("Failed to load image: ") + filename);
411 }
412
413 int const expected = static_cast<int>(glyphsTo) - static_cast<int>(glyphsFrom) + 1;
414 Color sep = resolveSeparator(mImage, config);
415
416 auto found = scanGlyphs(mImage, expected, sep, config.glyphPadding, config.verbose);
417
418 if (config.strategy == SeparatorStrategy::Auto && static_cast<int>(found.size()) < expected * 0.8) {
419 sep = getBorderDominantColor(mImage);
420 found = scanGlyphs(mImage, expected, sep, config.glyphPadding, config.verbose);
421 }
422
423 if (std::cmp_less(found.size(), expected)) {
424 std::ostringstream os;
425 os << "Image " << mFilename << " is corrupt or uses wrong separator.\n"
426 << "Expected: " << expected << " glyphs, Found: " << found.size() << "\n"
427 << "Detected separator: R:" << static_cast<int>(sep.r) << " G:" << static_cast<int>(sep.g)
428 << " B:" << static_cast<int>(sep.b) << "\n"
429 << "Suggestion: Use ExplicitColor strategy with magenta (255,0,255)";
430 throwException(os.str());
431 }
432
433 for (int i = 0; i < expected; ++i) {
434 unsigned char const glyph = static_cast<unsigned char>(static_cast<int>(glyphsFrom) + i);
435 mGlyph.at(glyph) = found.at(i);
436 }
437
438 mHeight = std::accumulate(found.begin(), found.end(), 0, [](int maxH, auto const & r) {
439 return std::max(maxH, r.height);
440 });
441
442 mImage->convertToDisplayFormat();
443 mRowSpacing = 0;
444 mGlyphSpacing = 0;
445 }
446
447 ImageFont::~ImageFont()
448 {
449 delete mImage;
450 }
451
452 int ImageFont::getWidth(unsigned char glyph) const
453 {
454 if (mGlyph.at(glyph).width == 0) {
455 return mGlyph.at(static_cast<int>(' ')).width + mGlyphSpacing;
456 }
457
458 return mGlyph.at(glyph).width + mGlyphSpacing;
459 }
460
462 {
463 return mHeight + mRowSpacing;
464 }
465
466 int ImageFont::drawGlyph(Graphics* graphics, unsigned char glyph, int x, int y)
467 {
468 // This is needed for drawing the glyph in the middle
469 // if we have spacing.
470 int const yoffset = getRowSpacing() / 2;
471
472 if (mGlyph.at(glyph).width == 0) {
473 graphics->drawRectangle(
474 x,
475 y + 1 + yoffset,
476 mGlyph.at(static_cast<int>(' ')).width - 1,
477 mGlyph.at(static_cast<int>(' ')).height - 2);
478
479 return mGlyph.at(static_cast<int>(' ')).width + mGlyphSpacing;
480 }
481
482 graphics->drawImage(
483 mImage,
484 mGlyph.at(glyph).x,
485 mGlyph.at(glyph).y,
486 x,
487 y + yoffset,
488 mGlyph.at(glyph).width,
489 mGlyph.at(glyph).height);
490
491 return mGlyph.at(glyph).width + mGlyphSpacing;
492 }
493
494 void ImageFont::drawString(Graphics* graphics, std::string const & text, int x, int y)
495 {
496 for (char const c : text) {
497 drawGlyph(graphics, c, x, y);
498 x += getWidth(c);
499 }
500 }
501
502 void ImageFont::setRowSpacing(int spacing)
503 {
504 mRowSpacing = spacing;
505 }
506
508 {
509 return mRowSpacing;
510 }
511
513 {
514 mGlyphSpacing = spacing;
515 }
516
518 {
519 return mGlyphSpacing;
520 }
521
522 Rectangle ImageFont::scanForGlyph(unsigned char glyph, int x, int y, Color const & separator)
523 {
524 Color color;
525
526 // Find glyph start
527 bool foundGlyphStart = false;
528
529 while (!foundGlyphStart) {
530 if (x >= mImage->getWidth()) {
531 x = 0;
532 y += mHeight + 1;
533
534 if (y >= mImage->getHeight()) {
535 std::ostringstream os;
536 os << "Image " << mFilename << " with font is corrupt near character '" << glyph << "'";
537 throwException(os.str());
538 }
539 }
540
541 color = mImage->getPixel(x, y);
542
543 foundGlyphStart = (color != separator);
544
545 if (!foundGlyphStart) {
546 ++x;
547 }
548 }
549
550 // Find glyph width
551 int width = 0;
552 bool foundGlyphEnd = false;
553
554 while (!foundGlyphEnd) {
555 if (x + width >= mImage->getWidth()) {
556 std::ostringstream os;
557 os << "Image " << mFilename << " with font is corrupt near character '" << glyph << "'";
558 throwException(os.str());
559 }
560
561 color = mImage->getPixel(x + width, y);
562
563 foundGlyphEnd = (color == separator);
564
565 if (!foundGlyphEnd) {
566 ++width;
567 }
568 }
569
570 // width now points to the separator pixel; glyph width is the measured width
571 return {x, y, width, mHeight};
572 }
573
574 int ImageFont::getWidth(std::string const & text) const
575 {
576 unsigned int i = 0;
577 int size = 0;
578
579 for (i = 0; i < text.size(); ++i) {
580 size += getWidth(text.at(i));
581 }
582
583 return size - mGlyphSpacing;
584 }
585
586 int ImageFont::getStringIndexAt(std::string const & text, int x) const
587 {
588 unsigned int i = 0;
589 int size = 0;
590
591 for (i = 0; i < text.size(); ++i) {
592 size += getWidth(text.at(i));
593
594 if (size > x) {
595 return i;
596 }
597 }
598
599 return text.size();
600 }
601} // namespace fcn
Color.
Definition color.hpp:58
uint8_t b
Blue color component (0-255).
Definition color.hpp:347
uint8_t g
Green color component (0-255).
Definition color.hpp:344
uint8_t r
Red color component (0-255).
Definition color.hpp:341
Abstract interface providing primitive drawing functions (lines, rectangles, etc.).
Definition graphics.hpp:58
virtual void drawImage(Image const *image, int srcX, int srcY, int dstX, int dstY, int width, int height)=0
Draws a part of an image.
virtual void drawRectangle(Rectangle const &rectangle)=0
Draws a simple, non-filled rectangle with a one pixel width.
virtual int getRowSpacing()
Gets the space between rows in pixels.
int mHeight
Holds the height of the image font.
virtual int drawGlyph(Graphics *graphics, unsigned char glyph, int x, int y)
Draws a glyph.
int getHeight() const override
Gets the height of the glyphs in the font.
int getStringIndexAt(std::string const &text, int x) const override
Gets a string index in a string providing an x coordinate.
Rectangle scanForGlyph(unsigned char glyph, int x, int y, Color const &separator)
Scans for a certain glyph.
Image * mImage
Holds the image with the font data.
std::array< Rectangle, 256 > mGlyph
Holds the glyphs areas in the image.
std::string mFilename
Holds the filename of the image with the font data.
virtual void setRowSpacing(int spacing)
Sets the space between rows in pixels.
ImageFont(std::string const &filename, std::string const &glyphs)
Constructor.
virtual int getWidth(unsigned char glyph) const
Gets a width of a glyph in pixels.
virtual int getGlyphSpacing()
Gets the spacing between letters in pixels.
virtual void setGlyphSpacing(int spacing)
Sets the spacing between glyphs in pixels.
int mGlyphSpacing
Holds the glyph spacing of the image font.
int mRowSpacing
Holds the row spacing of the image font.
void drawString(Graphics *graphics, std::string const &text, int x, int y) override
Draws a string.
Abstract holder for image data.
Definition image.hpp:34
Represents a rectangular area (X, Y, Width, Height).
Definition rectangle.hpp:22
int width
Holds the width of the rectangle.
int y
Holds the x coordinate of the rectangle.
int x
Holds the x coordinate of the rectangle.
int height
Holds the height of the rectangle.
Used replacement tokens by configure_file():
void throwException(std::string const &message, std::source_location location=std::source_location::current())
Throw an Exception capturing the current source location.
Configuration struct for ImageFont constructors.
Definition imagefont.hpp:59
bool verbose
If true, enable verbose debug output while scanning fonts.
Definition imagefont.hpp:78
SeparatorStrategy strategy
Strategy used to detect separator color in the image.
Definition imagefont.hpp:63
int glyphPadding
Number of pixels to pad/ignore around detected glyphs.
Definition imagefont.hpp:73