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