1/**
2 * Copyright (c) 2006-2023 LOVE Development Team
3 *
4 * This software is provided 'as-is', without any express or implied
5 * warranty. In no event will the authors be held liable for any damages
6 * arising from the use of this software.
7 *
8 * Permission is granted to anyone to use this software for any purpose,
9 * including commercial applications, and to alter it and redistribute it
10 * freely, subject to the following restrictions:
11 *
12 * 1. The origin of this software must not be misrepresented; you must not
13 * claim that you wrote the original software. If you use this software
14 * in a product, an acknowledgment in the product documentation would be
15 * appreciated but is not required.
16 * 2. Altered source versions must be plainly marked as such, and must not be
17 * misrepresented as being the original software.
18 * 3. This notice may not be removed or altered from any source distribution.
19 **/
20
21// LOVE
22#include "BMFontRasterizer.h"
23#include "filesystem/Filesystem.h"
24#include "image/Image.h"
25
26// C++
27#include <sstream>
28#include <vector>
29#include <algorithm>
30
31// C
32#include <cstdlib>
33#include <cstring>
34
35namespace love
36{
37namespace font
38{
39
40namespace
41{
42
43/**
44 * Helper class for parsing lines in BMFont definition files.
45 * NOTE: Does not properly handle multi-value attributes (e.g. 'padding' or
46 * 'spacing'.)
47 **/
48class BMFontLine
49{
50public:
51
52 BMFontLine(const std::string &line);
53
54 const std::string &getTag() const { return tag; }
55
56 int getAttributeInt(const char *name) const;
57 std::string getAttributeString(const char *name) const;
58
59private:
60
61 std::string tag;
62 std::unordered_map<std::string, std::string> attributes;
63
64};
65
66// This is not entirely robust...
67BMFontLine::BMFontLine(const std::string &line)
68{
69 // The tag name should always be at the start of the line.
70 tag = line.substr(0, line.find(' '));
71
72 size_t startpos = 0;
73
74 while (startpos < line.length())
75 {
76 // Find the next '=', which indicates a key-value pair.
77 size_t fpos = line.find('=', startpos);
78 if (fpos == std::string::npos || fpos + 1 >= line.length())
79 break;
80
81 // The key should be between a space character and the '='.
82 size_t keystart = line.rfind(' ', fpos);
83 if (keystart == std::string::npos)
84 break;
85
86 keystart++;
87
88 std::string key = line.substr(keystart, fpos - keystart);
89
90 size_t valstart = fpos + 1;
91 size_t valend = valstart + 1;
92
93 if (line[valstart] == '"')
94 {
95 // Values can be surrounded by quotes (a literal string.)
96 valstart++;
97 valend = line.find('"', valstart) - 1;
98 }
99 else
100 {
101 // Otherwise look for the next space character after the '='.
102 valend = line.find(' ', valstart + 1) - 1;
103 }
104
105 valend = std::min(valend, line.length() - 1);
106
107 attributes[key] = line.substr(valstart, valend - valstart + 1);
108
109 startpos = valend + 1;
110 }
111}
112
113int BMFontLine::getAttributeInt(const char *name) const
114{
115 auto it = attributes.find(name);
116 if (it == attributes.end())
117 return 0;
118
119 return (int) strtol(it->second.c_str(), nullptr, 10);
120}
121
122std::string BMFontLine::getAttributeString(const char *name) const
123{
124 auto it = attributes.find(name);
125 if (it == attributes.end())
126 return "";
127
128 return it->second;
129}
130
131} // anonymous namespace
132
133
134BMFontRasterizer::BMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &imagelist, float dpiscale)
135 : fontSize(0)
136 , unicode(false)
137 , lineHeight(0)
138{
139 this->dpiScale = dpiscale;
140
141 const std::string &filename = fontdef->getFilename();
142
143 size_t separatorpos = filename.rfind('/');
144 if (separatorpos != std::string::npos)
145 fontFolder = filename.substr(0, separatorpos);
146
147 // The parseConfig function will try to load any missing page images.
148 for (int i = 0; i < (int) imagelist.size(); i++)
149 {
150 if (imagelist[i]->getFormat() != PIXELFORMAT_RGBA8)
151 throw love::Exception("Only 32-bit RGBA images are supported in BMFonts.");
152
153 images[i] = imagelist[i];
154 }
155
156 std::string configtext((const char *) fontdef->getData(), fontdef->getSize());
157
158 parseConfig(configtext);
159}
160
161BMFontRasterizer::~BMFontRasterizer()
162{
163}
164
165void BMFontRasterizer::parseConfig(const std::string &configtext)
166{
167 std::stringstream ss(configtext);
168 std::string line;
169
170 while (std::getline(ss, line))
171 {
172 BMFontLine cline(line);
173
174 const std::string &tag = cline.getTag();
175
176 if (tag == "info")
177 {
178 fontSize = cline.getAttributeInt("size");
179 unicode = cline.getAttributeInt("unicode") > 0;
180 }
181 else if (tag == "common")
182 {
183 lineHeight = cline.getAttributeInt("lineHeight");
184 metrics.ascent = cline.getAttributeInt("base");
185 }
186 else if (tag == "page")
187 {
188 int pageindex = cline.getAttributeInt("id");
189 std::string filename = cline.getAttributeString("file");
190
191 // The file name is relative to the font file's folder.
192 if (!fontFolder.empty())
193 filename = fontFolder + "/" + filename;
194
195 // Load the page file from disk into an ImageData, if necessary.
196 if (images[pageindex].get() == nullptr)
197 {
198 using namespace love::filesystem;
199 using namespace love::image;
200
201 auto filesystem = Module::getInstance<Filesystem>(Module::M_FILESYSTEM);
202 auto imagemodule = Module::getInstance<image::Image>(Module::M_IMAGE);
203
204 if (!filesystem)
205 throw love::Exception("Filesystem module not loaded!");
206 if (!imagemodule)
207 throw love::Exception("Image module not loaded!");
208
209 // read() returns a retained ref already.
210 StrongRef<FileData> data(filesystem->read(filename.c_str()), Acquire::NORETAIN);
211
212 ImageData *imagedata = imagemodule->newImageData(data.get());
213
214 if (imagedata->getFormat() != PIXELFORMAT_RGBA8)
215 {
216 imagedata->release();
217 throw love::Exception("Only 32-bit RGBA images are supported in BMFonts.");
218 }
219
220 // Same with newImageData.
221 images[pageindex].set(imagedata, Acquire::NORETAIN);
222 }
223 }
224 else if (tag == "char")
225 {
226 BMFontCharacter c = {};
227
228 uint32 id = (uint32) cline.getAttributeInt("id");
229
230 c.x = cline.getAttributeInt("x");
231 c.y = cline.getAttributeInt("y");
232 c.page = cline.getAttributeInt("page");
233
234 c.metrics.width = cline.getAttributeInt("width");
235 c.metrics.height = cline.getAttributeInt("height");
236 c.metrics.bearingX = cline.getAttributeInt("xoffset");
237 c.metrics.bearingY = -cline.getAttributeInt("yoffset");
238 c.metrics.advance = cline.getAttributeInt("xadvance");
239
240 characters[id] = c;
241 }
242 else if (tag == "kerning")
243 {
244 uint32 firstid = (uint32) cline.getAttributeInt("first");
245 uint32 secondid = (uint32) cline.getAttributeInt("second");
246
247 uint64 packedids = ((uint64) firstid << 32) | (uint64) secondid;
248
249 kerning[packedids] = cline.getAttributeInt("amount");
250 }
251 }
252
253 if (characters.size() == 0)
254 throw love::Exception("Invalid BMFont file (no character definitions?)");
255
256 // Try to guess the line height if the lineheight attribute isn't found.
257 bool guessheight = lineHeight == 0;
258
259 // Verify the glyph character attributes.
260 for (const auto &cpair : characters)
261 {
262 const BMFontCharacter &c = cpair.second;
263 int width = c.metrics.width;
264 int height = c.metrics.height;
265
266 if (!unicode && cpair.first > 127)
267 throw love::Exception("Invalid BMFont character id (only unicode and ASCII are supported)");
268
269 if (c.page < 0 || images[c.page].get() == nullptr)
270 throw love::Exception("Invalid BMFont character page id: %d", c.page);
271
272 const image::ImageData *id = images[c.page].get();
273
274 if (!id->inside(c.x, c.y))
275 throw love::Exception("Invalid coordinates for BMFont character %u.", cpair.first);
276
277 if (width > 0 && !id->inside(c.x + width - 1, c.y))
278 throw love::Exception("Invalid width %d for BMFont character %u.", width, cpair.first);
279
280 if (height > 0 && !id->inside(c.x, c.y + height - 1))
281 throw love::Exception("Invalid height %d for BMFont character %u.", height, cpair.first);
282
283 if (guessheight)
284 lineHeight = std::max(lineHeight, c.metrics.height);
285 }
286
287 metrics.height = lineHeight;
288}
289
290int BMFontRasterizer::getLineHeight() const
291{
292 return lineHeight;
293}
294
295GlyphData *BMFontRasterizer::getGlyphData(uint32 glyph) const
296{
297 auto it = characters.find(glyph);
298
299 // Return an empty GlyphData if we don't have the glyph character.
300 if (it == characters.end())
301 return new GlyphData(glyph, GlyphMetrics(), PIXELFORMAT_RGBA8);
302
303 const BMFontCharacter &c = it->second;
304 const auto &imagepair = images.find(c.page);
305
306 if (imagepair == images.end())
307 return new GlyphData(glyph, GlyphMetrics(), PIXELFORMAT_RGBA8);
308
309 image::ImageData *imagedata = imagepair->second.get();
310 GlyphData *g = new GlyphData(glyph, c.metrics, PIXELFORMAT_RGBA8);
311
312 size_t pixelsize = imagedata->getPixelSize();
313
314 uint8 *pixels = (uint8 *) g->getData();
315 const uint8 *ipixels = (const uint8 *) imagedata->getData();
316
317 love::thread::Lock lock(imagedata->getMutex());
318
319 // Copy the subsection of the texture from the ImageData to the GlyphData.
320 for (int y = 0; y < c.metrics.height; y++)
321 {
322 size_t idindex = ((c.y + y) * imagedata->getWidth() + c.x) * pixelsize;
323 memcpy(&pixels[y * c.metrics.width * pixelsize], &ipixels[idindex], pixelsize * c.metrics.width);
324 }
325
326 return g;
327}
328
329int BMFontRasterizer::getGlyphCount() const
330{
331 return (int) characters.size();
332}
333
334bool BMFontRasterizer::hasGlyph(uint32 glyph) const
335{
336 return characters.find(glyph) != characters.end();
337}
338
339float BMFontRasterizer::getKerning(uint32 leftglyph, uint32 rightglyph) const
340{
341 uint64 packedglyphs = ((uint64) leftglyph << 32) | (uint64) rightglyph;
342
343 auto it = kerning.find(packedglyphs);
344 if (it != kerning.end())
345 return it->second;
346
347 return 0.0f;
348}
349
350Rasterizer::DataType BMFontRasterizer::getDataType() const
351{
352 return DATA_IMAGE;
353}
354
355bool BMFontRasterizer::accepts(love::filesystem::FileData *fontdef)
356{
357 const char *data = (const char *) fontdef->getData();
358
359 // Check if the "info" tag is at the start of the file. This is a truly
360 // crappy test. Is the tag even guaranteed to be at the start?
361 return fontdef->getSize() > 4 && memcmp(data, "info", 4) == 0;
362}
363
364} // font
365} // love
366