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 }