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 }