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 | |
35 | namespace love |
36 | { |
37 | namespace font |
38 | { |
39 | |
40 | namespace |
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 | **/ |
48 | class BMFontLine |
49 | { |
50 | public: |
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 | |
59 | private: |
60 | |
61 | std::string tag; |
62 | std::unordered_map<std::string, std::string> attributes; |
63 | |
64 | }; |
65 | |
66 | // This is not entirely robust... |
67 | BMFontLine::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 | |
113 | int 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 | |
122 | std::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 | |
134 | BMFontRasterizer::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 | |
161 | BMFontRasterizer::~BMFontRasterizer() |
162 | { |
163 | } |
164 | |
165 | void 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 | |
290 | int BMFontRasterizer::getLineHeight() const |
291 | { |
292 | return lineHeight; |
293 | } |
294 | |
295 | GlyphData *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 | |
329 | int BMFontRasterizer::getGlyphCount() const |
330 | { |
331 | return (int) characters.size(); |
332 | } |
333 | |
334 | bool BMFontRasterizer::hasGlyph(uint32 glyph) const |
335 | { |
336 | return characters.find(glyph) != characters.end(); |
337 | } |
338 | |
339 | float 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 | |
350 | Rasterizer::DataType BMFontRasterizer::getDataType() const |
351 | { |
352 | return DATA_IMAGE; |
353 | } |
354 | |
355 | bool 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 | |