1 /// AngelCode BMFont parser & generator 2 /// Copyright: Public Domain 3 /// Author: Jan Jurzitza 4 module bmfont; @safe: 5 6 import std.array : Appender, appender; 7 import std.ascii : isAlpha, isWhite; 8 import std.bitmanip : littleEndianToNative, nativeToLittleEndian; 9 import std.conv : to; 10 import std..string : representation, splitLines, strip, stripLeft; 11 import std.traits : isSomeString; 12 13 /// Information what each channel contains 14 enum ChannelType : ubyte 15 { 16 /// (0) This channel will contain the character 17 glyph = 0, 18 /// (1) This channel will contain the outline of the character 19 outline = 1, 20 /// (2) This channel will contain the character and outline 21 glyphAndOutline = 2, 22 /// (3) This channel will always be zero 23 zero = 3, 24 /// (4) This channel will always be one 25 one = 4 26 } 27 28 /// Bitfield on which channel a character is found 29 enum Channels : ubyte 30 { 31 /// 1 32 blue = 1, 33 /// 2 34 green = 2, 35 /// 4 36 red = 4, 37 /// 8 38 alpha = 8, 39 /// 15 40 all = 15 41 } 42 43 /// Flags for parsing 44 enum ParseFlags : ubyte 45 { 46 /// No special flags 47 none = 0, 48 /// The parser will ignore the info block 49 skipInfo = 1 << 0, 50 /// The parser will ignore the common block 51 skipCommon = 1 << 1, 52 /// The parser will ignore the kerning block 53 skipKerning = 1 << 2, 54 /// The parser will ignore the pages block 55 skipPages = 1 << 3, 56 /// Skips the info and common block 57 skipMeta = skipInfo | skipCommon, 58 /// Skips all blocks except the character block 59 skipNonChar = skipMeta | skipKerning | skipPages 60 } 61 62 private enum FontType : ubyte 63 { 64 none, 65 binary, 66 xml, 67 text 68 } 69 70 /// Generic Font struct containing all information from a BMFont file 71 struct Font 72 { 73 /// Info struct containing meta information about the exported font and export settings 74 struct Info 75 { 76 /// The size of the true type font 77 short fontSize; 78 /// bit 0: smooth, bit 1: unicode, bit 2: italic, bit 3: bold, bit 4: fixedHeigth, bits 5-7: reserved 79 ubyte bitField; 80 /// Charset identifier. Not used when parsing from a text file. 81 ubyte charSet; 82 /// The font height stretch as percentage. 100 means no stretch. 83 ushort stretchH; 84 /// The supersampling level used. 1 means no supersampling was used. 85 ubyte aa; 86 /// The padding for each character (up, right, down, left) 87 ubyte[4] padding; 88 /// The spacing for each character (horizontal, vertical) 89 ubyte[2] spacing; 90 /// The name of the true type font 91 string fontName; 92 /// The outline thickness for the characters 93 ubyte outline; 94 } 95 96 /// Struct containing common information about the font used for rendering 97 struct Common 98 { 99 /// This is the distance in pixels between each line of text 100 ushort lineHeight; 101 /// The number of pixels from the absolute top of the line to the base of the characters 102 ushort base; 103 /// The width of the texture, normally used to scale the x pos of the character image 104 ushort scaleW; 105 /// The height of the texture, normally used to scale the y pos of the character image 106 ushort scaleH; 107 /// The number of texture pages included in the font 108 ushort pages; 109 /// bits 0-6: reserved, bit 7: packed 110 ubyte bitField; 111 /// What information the alpha channel holds 112 ChannelType alphaChnl; 113 /// What information the red channel holds 114 ChannelType redChnl; 115 /// What information the green channel holds 116 ChannelType greenChnl; 117 /// What information the blue channel holds 118 ChannelType blueChnl; 119 } 120 121 /// Struct containing information about one character 122 struct Char 123 { 124 /// The character id 125 dchar id; 126 /// The left position of the character image in the texture 127 ushort x; 128 /// The top position of the character image in the texture 129 ushort y; 130 /// The width of the character image in the texture 131 ushort width; 132 /// The height of the character image in the texture 133 ushort height; 134 /// How much the current position should be offset when copying the image from the texture to the screen 135 short xoffset, yoffset; 136 /// How much the current position should be advanced after drawing the character 137 short xadvance; 138 /// The texture page where the character image is found 139 ubyte page; 140 /// The texture channel where the character image is found 141 Channels chnl; 142 } 143 144 /// Struct containing the kerning amount between 2 characters 145 struct Kerning 146 { 147 /// The first character id 148 dchar first; 149 /// The second character id 150 dchar second; 151 /// How much the x position should be adjusted when drawing the second character immediately following the first 152 short amount; 153 } 154 155 /// Version of the file 156 ubyte fileVersion; 157 /// Info struct containing meta information about the exported font and export settings 158 Info info; 159 /// Struct containing common information about the font used for rendering 160 Common common; 161 /// Array of page file locations 162 string[] pages; 163 /// Array containing information about all characters 164 Char[] chars; 165 /// Array containing all kerning pairs between characters which are not zero 166 Kerning[] kernings; 167 168 /// Type of the font data 169 FontType type = FontType.none; 170 171 /// Creates a text representation of this font 172 string toString() const @safe pure 173 { 174 import std.format; 175 176 string content = ""; 177 178 //dfmt off 179 content ~= format(`info face="%s" size=%d bold=%d italic=%d ` ~ 180 `charset="" unicode=%d stretchH=%d smooth=%d aa=%d padding=%(%d,%) ` ~ 181 `spacing=%(%d,%) outline=%d`, info.fontName.escape, info.fontSize, (info.bitField & 0b0001_0000) >> 4, 182 (info.bitField & 0b0010_0000) >> 5, (info.bitField & 0b0100_0000) >> 6, info.stretchH, 183 (info.bitField & 0b1000_0000) >> 7, info.aa, info.padding, info.spacing, info.outline) ~ '\n'; 184 content ~= format(`common lineHeight=%d base=%d scaleW=%d scaleH=%d ` ~ 185 `pages=%d packed=%d alphaChnl=%d redChnl=%d greenChnl=%d blueChnl=%d`, 186 common.lineHeight, common.base, common.scaleW, common.scaleH, pages.length, 187 (common.bitField & 0b0000_0001), cast(uint) common.alphaChnl, cast(uint) common.redChnl, 188 cast(uint) common.greenChnl, cast(uint) common.blueChnl) ~ '\n'; 189 //dfmt on 190 191 foreach (i, page; pages) 192 content ~= format(`page id=%d file="%s"`, i, page.escape) ~ '\n'; 193 194 content ~= "chars count=" ~ chars.length.to!string ~ '\n'; 195 196 foreach (Char c; chars) 197 content ~= format( 198 `char id=%d x=%d y=%d width=%d height=%d xoffset=%d yoffset=%d xadvance=%d page=%d chnl=%d`, 199 c.id, c.x, c.y, c.width, c.height, c.xoffset, c.yoffset, 200 c.xadvance, c.page, cast(uint) c.chnl) ~ '\n'; 201 202 content ~= "kernings count=" ~ kernings.length.to!string ~ '\n'; 203 204 foreach (Kerning k; kernings) 205 content ~= format(`kerning first=%d second=%d amount=%d`, 206 cast(uint) k.first, cast(uint) k.second, k.amount) ~ '\n'; 207 208 return content; 209 } 210 211 /// Creates a binary representation of this font 212 ubyte[] toBinary() const @safe pure 213 { 214 import std..string : toStringz; 215 216 auto header = appender(cast(ubyte[])[66, 77, 70, 3]); // BMF v3 217 auto binfo = appender!(ubyte[]); 218 auto bcommon = appender!(ubyte[]); 219 auto bpages = appender!(ubyte[]); 220 auto bchars = appender!(ubyte[]); 221 auto bkernings = appender!(ubyte[]); 222 223 binfo.putRange(nativeToLittleEndian(info.fontSize)); 224 binfo.put(info.bitField); 225 binfo.put(info.charSet); 226 binfo.putRange(nativeToLittleEndian(info.stretchH)); 227 binfo.put(info.aa); 228 binfo.putRange(info.padding); 229 binfo.putRange(info.spacing); 230 binfo.put(info.outline); 231 binfo.putRange(info.fontName); 232 binfo.put(cast(ubyte) 0u); 233 234 bcommon.putRange(nativeToLittleEndian(common.lineHeight)); 235 bcommon.putRange(nativeToLittleEndian(common.base)); 236 bcommon.putRange(nativeToLittleEndian(common.scaleW)); 237 bcommon.putRange(nativeToLittleEndian(common.scaleH)); 238 bcommon.putRange(nativeToLittleEndian(common.pages)); 239 bcommon.put(common.bitField); 240 bcommon.put(common.alphaChnl); 241 bcommon.put(common.redChnl); 242 bcommon.put(common.greenChnl); 243 bcommon.put(common.blueChnl); 244 245 foreach (page; pages) 246 { 247 bpages.putRange(page); 248 bpages.put(cast(ubyte) 0u); 249 } 250 251 foreach (c; chars) 252 { 253 bchars.putRange(nativeToLittleEndian(cast(uint) c.id)); 254 bchars.putRange(nativeToLittleEndian(c.x)); 255 bchars.putRange(nativeToLittleEndian(c.y)); 256 bchars.putRange(nativeToLittleEndian(c.width)); 257 bchars.putRange(nativeToLittleEndian(c.height)); 258 bchars.putRange(nativeToLittleEndian(c.xoffset)); 259 bchars.putRange(nativeToLittleEndian(c.yoffset)); 260 bchars.putRange(nativeToLittleEndian(c.xadvance)); 261 bchars.put(c.page); 262 bchars.put(c.chnl); 263 } 264 265 foreach (k; kernings) 266 { 267 bkernings.putRange(nativeToLittleEndian(cast(uint) k.first)); 268 bkernings.putRange(nativeToLittleEndian(cast(uint) k.second)); 269 bkernings.putRange(nativeToLittleEndian(k.amount)); 270 } 271 272 foreach (i, block; [binfo, bcommon, bpages, bchars, bkernings]) 273 { 274 header.put(cast(ubyte)(i + 1)); 275 header.putRange(nativeToLittleEndian(cast(uint) block.data.length)); 276 header.put(block.data); 277 } 278 279 return header.data; 280 } 281 282 /// Returns: Information about the character passed as argument `c`. Empty Char struct with dchar.init as id if not found. 283 Char getChar(dchar id) const nothrow pure 284 { 285 foreach (c; chars) 286 if (c.id == id) 287 return c; 288 return Char(); 289 } 290 291 /// Returns: the kerning between two characters. This is the additional distance the `second` character should be moved if the character before that is `first` 292 short getKerning(dchar first, dchar second) const nothrow pure 293 { 294 foreach (kerning; kernings) 295 if (kerning.first == first && kerning.second == second) 296 return kerning.amount; 297 return 0; 298 } 299 } 300 301 private void putRange(size_t n)(ref Appender!(ubyte[]) dst, ubyte[n] src) pure 302 { 303 foreach (b; src) 304 dst.put(b); 305 } 306 307 private void putRange(ref Appender!(ubyte[]) dst, scope const(char)[] src) pure 308 { 309 foreach (char c; src) 310 dst.put(cast(ubyte) c); 311 } 312 313 private string escape(in string s) pure nothrow @safe 314 { 315 import std..string : replace; 316 317 return s.replace("\\", "\\\\").replace("\"", "\\\""); 318 } 319 320 private const(ubyte)[] getReadonlyBytes(T)(return scope T[] data) pure 321 if (T.sizeof == 1) 322 { 323 return (() @trusted => cast(const(ubyte)[]) data)(); 324 } 325 326 /// Gets thrown if a Font is in an invalid format 327 class InvalidFormatException : Exception 328 { 329 this(string msg = "Font is in an invalid format", string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe 330 { 331 super(msg, file, line); 332 } 333 } 334 335 /// Parses a font and automatically figures out if its binary or text. Pass additional ParseFlags to skip sections. 336 Font parseFnt(T)(auto ref in T data, ParseFlags flags = ParseFlags.none) pure 337 if (isSomeString!T || is(T == ubyte[])) 338 { 339 Font font; 340 341 ubyte[] buffer; 342 ubyte currentBlock = 0; 343 uint blockLength = 0; 344 uint skipRemaining = 0; 345 bool parseFontName = false; 346 size_t curPage = 0; 347 uint pageTotal = 0; 348 if (data[0 .. 3].getReadonlyBytes == "BMF".representation) 349 { 350 font.type = FontType.binary; 351 font.fileVersion = data[3]; 352 if (font.fileVersion != 3) 353 throw new InvalidFormatException( 354 "Font version is not supported: " ~ font.fileVersion.to!string); 355 } 356 else if (data[0 .. 4].getReadonlyBytes == "<?xm".representation) 357 font.type = FontType.xml; 358 else 359 font.type = FontType.text; 360 final switch (font.type) 361 { 362 case FontType.binary: 363 foreach (c; data) 364 { 365 if (skipRemaining > 0) 366 { 367 skipRemaining--; 368 continue; 369 } 370 buffer ~= cast(ubyte) c; 371 switch (currentBlock) 372 { 373 case ubyte.max: // to be determined 374 if (buffer.length == 5) 375 { 376 currentBlock = buffer[0]; 377 blockLength = littleEndianToNative!uint(buffer[1 .. 5]); 378 buffer.length = 0; 379 380 if ((flags & ParseFlags.skipInfo && currentBlock == 1) 381 || (flags & ParseFlags.skipCommon && currentBlock == 2) 382 || (flags & ParseFlags.skipPages && currentBlock == 3) 383 || (flags & ParseFlags.skipKerning && currentBlock == 5)) 384 { 385 skipRemaining = blockLength; 386 currentBlock = ubyte.max; 387 } 388 } 389 else if (buffer.length > 5) 390 throw new InvalidFormatException(); 391 break; 392 case 0: // header 393 if (buffer.length == 4) 394 { 395 396 currentBlock = ubyte.max; 397 buffer.length = 0; 398 } 399 else if (buffer.length > 4) 400 throw new InvalidFormatException(); 401 break; 402 case 1: // info 403 if (parseFontName) 404 { 405 if (buffer[$ - 1] == 0) 406 { 407 currentBlock = ubyte.max; 408 buffer.length = 0; 409 parseFontName = false; 410 } 411 else 412 font.info.fontName ~= cast(char) buffer[$ - 1]; 413 } 414 else 415 { 416 if (buffer.length == 14) 417 { 418 font.info.fontSize = littleEndianToNative!short(buffer[0 .. 2]); 419 font.info.bitField = buffer[2]; 420 font.info.charSet = buffer[3]; 421 font.info.stretchH = littleEndianToNative!ushort(buffer[4 .. 6]); 422 font.info.aa = buffer[6]; 423 font.info.padding = buffer[7 .. 11]; 424 font.info.spacing = buffer[11 .. 13]; 425 font.info.outline = buffer[13]; 426 parseFontName = true; 427 } 428 else if (buffer.length > 14) 429 throw new InvalidFormatException(); 430 } 431 break; 432 case 2: // common 433 if (buffer.length == 15) 434 { 435 font.common.lineHeight = littleEndianToNative!ushort(buffer[0 .. 2]); 436 font.common.base = littleEndianToNative!ushort(buffer[2 .. 4]); 437 font.common.scaleW = littleEndianToNative!ushort(buffer[4 .. 6]); 438 font.common.scaleH = littleEndianToNative!ushort(buffer[6 .. 8]); 439 font.common.pages = littleEndianToNative!ushort(buffer[8 .. 10]); 440 if ((flags & ParseFlags.skipPages) == 0) 441 font.pages.length = font.common.pages; 442 font.common.bitField = buffer[10]; 443 font.common.alphaChnl = cast(ChannelType) buffer[11]; 444 font.common.redChnl = cast(ChannelType) buffer[12]; 445 font.common.greenChnl = cast(ChannelType) buffer[13]; 446 font.common.blueChnl = cast(ChannelType) buffer[14]; 447 currentBlock = ubyte.max; 448 buffer.length = 0; 449 } 450 else if (buffer.length > 15) 451 throw new InvalidFormatException(); 452 break; 453 case 3: // pages 454 if (pageTotal > blockLength) 455 throw new Exception("Parse error. Please report!"); 456 if (buffer[$ - 1] == 0) 457 { 458 font.pages[curPage] = cast(string) buffer[0 .. $ - 1].idup; 459 curPage++; 460 pageTotal += buffer.length; 461 buffer.length = 0; 462 } 463 if (pageTotal == blockLength) 464 { 465 assert(buffer.length == 0, 466 "String ended wrong! Still in buffer: " ~ buffer.to!string); 467 currentBlock = ubyte.max; 468 buffer.length = 0; 469 break; 470 } 471 break; 472 case 4: // chars 473 if (buffer.length == 20) 474 { 475 Font.Char charInfo; 476 charInfo.id = cast(dchar) littleEndianToNative!uint(buffer[0 .. 4]); 477 charInfo.x = littleEndianToNative!ushort(buffer[4 .. 6]); 478 charInfo.y = littleEndianToNative!ushort(buffer[6 .. 8]); 479 charInfo.width = littleEndianToNative!ushort(buffer[8 .. 10]); 480 charInfo.height = littleEndianToNative!ushort(buffer[10 .. 12]); 481 charInfo.xoffset = littleEndianToNative!short(buffer[12 .. 14]); 482 charInfo.yoffset = littleEndianToNative!short(buffer[14 .. 16]); 483 charInfo.xadvance = littleEndianToNative!short(buffer[16 .. 18]); 484 charInfo.page = buffer[18]; 485 charInfo.chnl = cast(Channels) buffer[19]; 486 font.chars ~= charInfo; 487 buffer.length = 0; 488 } 489 else if (buffer.length > 20) 490 throw new Exception("Skipped some bytes. Please report."); 491 if (font.chars.length == blockLength / 20) 492 { 493 currentBlock = ubyte.max; 494 buffer.length = 0; 495 break; 496 } 497 break; 498 case 5: // kernings 499 if (buffer.length == 10) 500 { 501 Font.Kerning kerning; 502 kerning.first = cast(dchar) littleEndianToNative!uint(buffer[0 .. 4]); 503 kerning.second = cast(dchar) littleEndianToNative!uint(buffer[4 .. 8]); 504 kerning.amount = littleEndianToNative!short(buffer[8 .. 10]); 505 font.kernings ~= kerning; 506 buffer.length = 0; 507 } 508 else if (buffer.length > 20) 509 throw new Exception("Skipped some bytes. Please report."); 510 if (font.kernings.length == blockLength / 10) 511 { 512 currentBlock = ubyte.max; 513 buffer.length = 0; 514 break; 515 } 516 break; 517 default: 518 throw new InvalidFormatException("Unknown block: " ~ currentBlock.to!string); 519 } 520 } 521 break; 522 case FontType.text: 523 foreach (line; (cast(const(char)[]) data).splitLines) 524 { 525 string type; 526 foreach (c; line) 527 { 528 if (isWhite(c)) 529 break; 530 type ~= c; 531 } 532 line = line[type.length .. $].stripLeft; 533 const(char)[][2][] arguments = line.getArguments(); 534 ushort pageID = 0; 535 switch (type) 536 { 537 case "info": 538 if (flags & ParseFlags.skipInfo) 539 break; 540 foreach (argument; arguments) 541 { 542 switch (argument[0]) 543 { 544 case "face": 545 font.info.fontName = argument[1].idup; 546 break; 547 case "size": 548 font.info.fontSize = argument[1].to!short; 549 break; 550 case "bold": 551 font.info.bitField |= argument[1] == "1" ? 0b0001_0000 : 0; 552 break; 553 case "italic": 554 font.info.bitField |= argument[1] == "1" ? 0b0010_0000 : 0; 555 break; 556 case "charset": 557 // TODO 558 break; 559 case "unicode": 560 font.info.bitField |= argument[1] == "1" ? 0b0100_0000 : 0; 561 break; 562 case "stretchH": 563 font.info.stretchH = argument[1].to!ushort; 564 break; 565 case "smooth": 566 font.info.bitField |= argument[1] == "1" ? 0b1000_0000 : 0; 567 break; 568 case "aa": 569 font.info.aa = cast(ubyte) argument[1].to!uint; 570 break; 571 case "padding": 572 font.info.padding = ("[" ~ argument[1] ~ "]").to!(ubyte[]); 573 break; 574 case "spacing": 575 font.info.spacing = ("[" ~ argument[1] ~ "]").to!(ubyte[]); 576 break; 577 case "outline": 578 font.info.outline = cast(ubyte) argument[1].to!uint; 579 break; 580 default: 581 throw new InvalidFormatException("Unkown info argument: " ~ argument[0].to!string); 582 } 583 } 584 break; 585 case "common": 586 if (flags & ParseFlags.skipCommon) 587 break; 588 foreach (argument; arguments) 589 { 590 switch (argument[0]) 591 { 592 case "lineHeight": 593 font.common.lineHeight = argument[1].to!ushort; 594 break; 595 case "base": 596 font.common.base = argument[1].to!ushort; 597 break; 598 case "scaleW": 599 font.common.scaleW = argument[1].to!ushort; 600 break; 601 case "scaleH": 602 font.common.scaleH = argument[1].to!ushort; 603 break; 604 case "pages": 605 font.common.pages = argument[1].to!ushort; 606 font.pages.length = font.common.pages; 607 break; 608 case "packed": 609 font.common.bitField |= argument[1] == "1" ? 0b0000_0001 : 0; 610 break; 611 case "alphaChnl": 612 font.common.alphaChnl = cast(ChannelType) argument[1].to!uint; 613 break; 614 case "redChnl": 615 font.common.redChnl = cast(ChannelType) argument[1].to!uint; 616 break; 617 case "greenChnl": 618 font.common.greenChnl = cast(ChannelType) argument[1].to!uint; 619 break; 620 case "blueChnl": 621 font.common.blueChnl = cast(ChannelType) argument[1].to!uint; 622 break; 623 default: 624 throw new InvalidFormatException("Unkown common argument: " ~ argument[0].to!string); 625 } 626 } 627 break; 628 case "page": 629 if (flags & ParseFlags.skipPages) 630 break; 631 foreach (argument; arguments) 632 { 633 switch (argument[0]) 634 { 635 case "id": 636 pageID = argument[1].to!ushort; 637 break; 638 case "file": 639 font.pages[pageID] = argument[1].idup; 640 break; 641 default: 642 throw new InvalidFormatException("Unkown page argument: " ~ argument[0].to!string); 643 } 644 } 645 break; 646 case "chars": 647 if (arguments.length != 1) 648 throw new InvalidFormatException(); 649 font.chars.reserve(arguments[0][1].to!int); 650 break; 651 case "char": 652 Font.Char currChar; 653 foreach (argument; arguments) 654 { 655 switch (argument[0]) 656 { 657 case "id": 658 currChar.id = cast(dchar) argument[1].to!uint; 659 break; 660 case "x": 661 currChar.x = argument[1].to!ushort; 662 break; 663 case "y": 664 currChar.y = argument[1].to!ushort; 665 break; 666 case "width": 667 currChar.width = argument[1].to!ushort; 668 break; 669 case "height": 670 currChar.height = argument[1].to!ushort; 671 break; 672 case "xoffset": 673 currChar.xoffset = argument[1].to!short; 674 break; 675 case "yoffset": 676 currChar.yoffset = argument[1].to!short; 677 break; 678 case "xadvance": 679 currChar.xadvance = argument[1].to!short; 680 break; 681 case "page": 682 currChar.page = cast(ubyte) argument[1].to!uint; 683 break; 684 case "chnl": 685 currChar.chnl = cast(Channels) argument[1].to!uint; 686 break; 687 default: 688 throw new InvalidFormatException( 689 "Unkown char argument: " ~ argument.to!string); 690 } 691 } 692 font.chars ~= currChar; 693 break; 694 case "kernings": 695 if (flags & ParseFlags.skipKerning) 696 break; 697 if (arguments.length != 1) 698 throw new InvalidFormatException(); 699 font.kernings.reserve(arguments[0][1].to!int); 700 break; 701 case "kerning": 702 if (flags & ParseFlags.skipKerning) 703 break; 704 Font.Kerning kerning; 705 foreach (argument; arguments) 706 { 707 switch (argument[0]) 708 { 709 case "first": 710 kerning.first = cast(dchar) argument[1].to!uint; 711 break; 712 case "second": 713 kerning.second = cast(dchar) argument[1].to!uint; 714 break; 715 case "amount": 716 kerning.amount = argument[1].to!short; 717 break; 718 default: 719 throw new InvalidFormatException("Unkown kerning argument: " ~ argument[0].to!string); 720 } 721 } 722 font.kernings ~= kerning; 723 break; 724 default: 725 throw new InvalidFormatException(); 726 } 727 } 728 break; 729 case FontType.xml: 730 throw new Exception("xml parsing is not yet implemented"); 731 case FontType.none: 732 throw new InvalidFormatException(); 733 } 734 735 return font; 736 } 737 738 private const(char)[][2][] getArguments(ref scope const(char)[] line) pure @safe 739 { 740 const(char)[][2][] args; 741 const(char)[][2] currArg = ""; 742 bool inString = false; 743 bool escape = false; 744 bool isKey = true; 745 foreach (c; line) 746 { 747 if (isKey) 748 { 749 if (c == '=') 750 { 751 isKey = false; 752 } 753 else if (c.isWhite && currArg[0].strip.length) 754 { 755 currArg[0] = currArg[0].strip; 756 currArg[1] = currArg[1].strip; 757 args ~= currArg; 758 isKey = true; 759 currArg[0] = ""; 760 currArg[1] = ""; 761 } 762 else 763 currArg[0] ~= c; 764 } 765 else 766 { 767 if (inString) 768 { 769 if (escape) 770 { 771 currArg[1] ~= c; 772 escape = false; 773 } 774 else 775 { 776 if (c == '"') 777 inString = false; 778 else if (c == '\\') 779 escape = true; 780 else 781 { 782 currArg[1] ~= c; 783 } 784 } 785 } 786 else 787 { 788 if (c.isWhite) 789 { 790 currArg[0] = currArg[0].strip; 791 currArg[1] = currArg[1].strip; 792 if (currArg[0].length) 793 { 794 args ~= currArg; 795 isKey = true; 796 currArg[0] = ""; 797 currArg[1] = ""; 798 } 799 } 800 else if (c == '"') 801 inString = true; 802 else 803 currArg[1] ~= c; 804 } 805 } 806 } 807 if (currArg[0].strip.length) 808 { 809 currArg[0] = currArg[0].strip; 810 currArg[1] = currArg[1].strip; 811 args ~= currArg; 812 } 813 return args; 814 } 815 816 /// 817 unittest 818 { 819 auto font = parseFnt(`info face="Roboto" size=32 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=1,1,1,1 spacing=1,1 outline=0 820 common lineHeight=33 base=26 scaleW=768 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4 821 page id=0 file="roboto_txt_0.png" 822 chars count=2 823 char id=97 x=383 y=452 width=15 height=16 xoffset=0 yoffset=11 xadvance=15 page=0 chnl=15 824 char id=98 x=554 y=239 width=16 height=22 xoffset=0 yoffset=5 xadvance=15 page=0 chnl=15 825 kernings count=1 826 kerning first=97 second=98 amount=-1 827 `); 828 assert(font.info.aa == 1); 829 assert(font.info.bitField == 0b1100_0000); 830 assert(font.info.charSet == 0); 831 assert(font.info.fontName == "Roboto"); 832 assert(font.info.fontSize == 32); 833 assert(font.info.padding == [1, 1, 1, 1]); 834 assert(font.info.spacing == [1, 1]); 835 assert(font.info.stretchH == 100); 836 837 assert(font.common.alphaChnl == ChannelType.glyph); 838 assert(font.common.base == 26); 839 assert(font.common.bitField == 0b0000_0000); 840 assert(font.common.blueChnl == ChannelType.one); 841 assert(font.common.greenChnl == ChannelType.one); 842 assert(font.common.lineHeight == 33); 843 assert(font.common.pages == 1); 844 assert(font.common.redChnl == ChannelType.one); 845 assert(font.common.scaleH == 512); 846 assert(font.common.scaleW == 768); 847 848 assert(font.chars.length == 2); 849 assert(font.chars[0].chnl == Channels.all); 850 assert(font.chars[0].height == 16); 851 assert(font.chars[0].id == 'a'); 852 assert(font.chars[0].page == 0); 853 assert(font.chars[0].width == 15); 854 assert(font.chars[0].x == 383); 855 assert(font.chars[0].xadvance == 15); 856 assert(font.chars[0].xoffset == 0); 857 assert(font.chars[0].y == 452); 858 assert(font.chars[0].yoffset == 11); 859 860 assert(font.chars[1].chnl == Channels.all); 861 assert(font.chars[1].height == 22); 862 assert(font.chars[1].id == 'b'); 863 assert(font.chars[1].page == 0); 864 assert(font.chars[1].width == 16); 865 assert(font.chars[1].x == 554); 866 assert(font.chars[1].xadvance == 15); 867 assert(font.chars[1].xoffset == 0); 868 assert(font.chars[1].y == 239); 869 assert(font.chars[1].yoffset == 5); 870 871 assert(font.kernings.length == 1); 872 assert(font.kernings[0].first == 'a'); 873 assert(font.kernings[0].second == 'b'); 874 assert(font.kernings[0].amount == -1); 875 } 876 877 @system unittest 878 { 879 import std.file; 880 881 auto binary = parseFnt(cast(ubyte[]) read("test/roboto.fnt")); 882 auto text = parseFnt(readText("test/roboto_txt.fnt")); 883 assert(binary.info == text.info); 884 assert(binary.common == text.common); 885 assert(binary.kernings == text.kernings); 886 assert(binary.chars == text.chars); 887 assert(binary.type != text.type); 888 } 889 890 @system unittest 891 { 892 import std.file; 893 894 auto binary = parseFnt(cast(ubyte[]) read("test/roboto.fnt")); 895 auto text = parseFnt(parseFnt(readText("test/roboto_txt.fnt")).toString()); 896 assert(binary.info == text.info); 897 assert(binary.common == text.common); 898 assert(binary.kernings == text.kernings); 899 assert(binary.chars == text.chars); 900 assert(binary.type != text.type); 901 } 902 903 @system unittest 904 { 905 import std.file; 906 907 auto binary = parseFnt(parseFnt(cast(ubyte[]) read("test/roboto.fnt")).toBinary()); 908 auto text = parseFnt(readText("test/roboto_txt.fnt")); 909 assert(binary.info == text.info); 910 assert(binary.common == text.common); 911 assert(binary.kernings == text.kernings); 912 assert(binary.chars == text.chars); 913 assert(binary.type != text.type); 914 }