| 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 | |