Skip to content

Commit

Permalink
Support for all BMFont output file types in FlxBitmapFont (#2949)
Browse files Browse the repository at this point in the history
* Added support for all bmfont file types (xml, text, binary)

* Update FlxAngelCodeXmlAsset to also accept Bytes

* Cleanup of the `guessType` function and renamed FlxAngelCodeXmlAsset to FlxAngelCodeAsset

* Codeclimate fixes

* Unified the bmfont parsing functions into one function

* fix codeclimate

* Removed null types

* crete abstracts over typedefs, move xml parsing

* move text parsing to abstracts

* rename package

* rename data classes

* move bytes parsing to abstracts

* move to separate modules

* separate fields for letter and id

* add doc

* remove redundnat local fields

* change field order and add padding/spacing type

* new regex for bmfont attribute matching. start of test cases

* better key/value attribute parser

* add spacing/padding fromBytes()

* fix ci

* remove unused function

* add BMFont text format test

* update BMFontTest to include binary file test

* formatting

---------

Co-authored-by: George FunBook <[email protected]>
Co-authored-by: Logan <[email protected]>
  • Loading branch information
3 people authored Dec 15, 2023
1 parent 211e212 commit f68d033
Show file tree
Hide file tree
Showing 10 changed files with 1,102 additions and 83 deletions.
114 changes: 33 additions & 81 deletions flixel/graphics/frames/FlxBitmapFont.hx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package flixel.graphics.frames;

import openfl.display.BitmapData;
import openfl.geom.Point;
import openfl.geom.Rectangle;
import flixel.FlxG;
import flixel.graphics.FlxGraphic;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.graphics.frames.FlxFramesCollection.FlxFrameCollectionType;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxAssets;
import flixel.util.FlxColor;
import openfl.Assets;
import haxe.xml.Access;
import openfl.Assets;
import openfl.display.BitmapData;
import openfl.geom.Point;
import openfl.geom.Rectangle;

using flixel.util.FlxUnicodeUtil;

Expand Down Expand Up @@ -161,19 +161,17 @@ class FlxBitmapFont extends FlxFramesCollection
* @param data Font data.
* @return Generated bitmap font object.
*/
public static function fromAngelCode(source:FlxBitmapFontGraphicAsset, data:FlxAngelCodeXmlAsset):FlxBitmapFont
public static function fromAngelCode(source:FlxBitmapFontGraphicAsset, data:FlxAngelCodeAsset):FlxBitmapFont
{
var graphic:FlxGraphic = null;
var frame:FlxFrame = null;

if ((source is FlxFrame))
{
frame = cast source;
graphic = frame.parent;
}
else
{
graphic = FlxG.bitmap.add(cast source);
final graphic = FlxG.bitmap.add(cast source);
frame = graphic.imageFrame.frame;
}

Expand All @@ -183,88 +181,42 @@ class FlxBitmapFont extends FlxFramesCollection

font = new FlxBitmapFont(frame);

final fast = new Access(data.getXml().firstElement());
final fontInfo = data.parse();

// how much to move the cursor when going to the next line.
font.lineHeight = Std.parseInt(fast.node.common.att.lineHeight);
font.size = Std.parseInt(fast.node.info.att.size);
font.fontName = Std.string(fast.node.info.att.face);
font.bold = (Std.parseInt(fast.node.info.att.bold) != 0);
font.italic = (Std.parseInt(fast.node.info.att.italic) != 0);

var frame:FlxRect;
var frameHeight:Int;
var offset:FlxPoint;
var charStr:String;
var charCode:Int;
var xOffset:Int, yOffset:Int, xAdvance:Int;

var chars = fast.node.chars;

for (char in chars.nodes.char)
font.lineHeight = fontInfo.common.lineHeight;
font.size = fontInfo.info.size;
font.fontName = fontInfo.info.face;
font.bold = fontInfo.info.bold;
font.italic = fontInfo.info.italic;

for (char in fontInfo.chars)
{
frame = FlxRect.get();
frame.x = Std.parseInt(char.att.x); // X position within the bitmap image file.
frame.y = Std.parseInt(char.att.y); // Y position within the bitmap image file.
frame.width = Std.parseInt(char.att.width); // Width of the character in the image file.
frameHeight = Std.parseInt(char.att.height);
frame.height = frameHeight; // Height of the character in the image file.

// Number of pixels to move right before drawing this character.
xOffset = char.has.xoffset ? Std.parseInt(char.att.xoffset) : 0;
// Number of pixels to move down before drawing this character.
yOffset = char.has.yoffset ? Std.parseInt(char.att.yoffset) : 0;
// Number of pixels to jump right after drawing this character.
xAdvance = char.has.xadvance ? Std.parseInt(char.att.xadvance) : 0;

offset = FlxPoint.get(xOffset, yOffset);

font.minOffsetX = (font.minOffsetX < -xOffset) ? -xOffset : font.minOffsetX;

charCode = -1;
charStr = null;

if (char.has.letter) // The ASCII value of the character this line is describing. Helpful for debugging
final frame = FlxRect.get();
frame.x = char.x; // X position within the bitmap image file.
frame.y = char.y; // Y position within the bitmap image file.
frame.width = char.width; // Width of the character in the image file.
frame.height = char.height; // Height of the character in the image file.

font.minOffsetX = (font.minOffsetX < -char.xoffset) ? -char.xoffset : font.minOffsetX;

if (char.id == -1)
{
charStr = char.att.letter;
throw 'Invalid font data!';
}
else if (char.has.id) // The character number in the ASCII table.

font.addCharFrame(char.id, frame, FlxPoint.get(char.xoffset, char.yoffset), char.xadvance);

if (char.id == SPACE_CODE)
{
charCode = Std.parseInt(char.att.id);
}

if (charCode == -1 && charStr == null)
{
throw 'Invalid font xml data!';
}

if (charStr != null)
{
charStr = switch (charStr)
{
case "space": ' ';
case "&quot;": '"';
case "&amp;": '&';
case "&gt;": '>';
case "&lt;": '<';
default: charStr;
}

charCode = charStr.uCharCodeAt(0);
}

font.addCharFrame(charCode, frame, offset, xAdvance);

if (charCode == SPACE_CODE)
{
font.spaceWidth = xAdvance;
font.spaceWidth = char.xadvance;
}
else
{
font.lineHeight = (font.lineHeight > frameHeight + yOffset) ? font.lineHeight : frameHeight + yOffset;
font.lineHeight = (font.lineHeight > char.height + char.yoffset) ? font.lineHeight : char.height + char.yoffset;
}
}

font.updateSourceHeight();
return font;
}
Expand Down
232 changes: 232 additions & 0 deletions flixel/graphics/frames/bmfont/BMFont.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package flixel.graphics.frames.bmfont;

import flixel.system.FlxAssets;
import haxe.io.Bytes;
import haxe.io.BytesInput;
import haxe.xml.Access;
import openfl.utils.Assets;

using StringTools;

/**
* Used internally via `FlxBitmapFont.fromAngelCode` to serialize text, xml or binary files
* exported from [BMFont](https://www.angelcode.com/products/bmfont/)
*
* @since 5.6.0
* @see [flixel.graphics.frames.FlxBitmapFont.fromAngelCode](https://api.haxeflixel.com/flixel/graphics/frames/FlxBitmapFont.html#fromAngelCode)
*/
class BMFont
{
public var info:BMFontInfo;
public var common:BMFontCommon;
public var pages:Array<BMFontPage>;
public var chars:Array<BMFontChar>;
public var kerning:Null<Array<BMFontKerning>> = null;

function new(?info, ?common, ?pages, ?chars, ?kerning)
{
this.info = info;
this.common = common;
this.pages = pages;
this.chars = chars;
this.kerning = kerning;
}

public static function fromXml(xml:Xml)
{
final xmlAccess = new Access(xml);
final info = BMFontInfo.fromXml(xmlAccess.node.info);
final common = BMFontCommon.fromXml(xmlAccess.node.common);
final pages = BMFontPage.listFromXml(xmlAccess.node.pages);
final chars = BMFontChar.listFromXml(xmlAccess.node.chars);
var kerning:Array<BMFontKerning> = null;

if (xmlAccess.hasNode.kernings)
{
kerning = BMFontKerning.listFromXml(xmlAccess.node.kernings);
}

return new BMFont(info, common, pages, chars, kerning);
}

public static function fromText(text:String)
{
var info:BMFontInfo = null;
var common:BMFontCommon = null;
final pages = new Array<BMFontPage>();
final chars = new Array<BMFontChar>();
final kernings = new Array<BMFontKerning>();
// we dont need these but they exists in the file
// var charCount = 0;
// var kerningCount = 0;

final lines = text.replace('\r\n', '\n').split('\n').filter((line) -> line.length > 0);
for (line in lines)
{
final blockType = line.substring(0, line.indexOf(' '));
final blockAttrs = line.substring(line.indexOf(' ') + 1);
switch blockType
{
case 'info': info = BMFontInfo.fromText(blockAttrs);
case 'common': common = BMFontCommon.fromText(blockAttrs);
case 'page': pages.push(BMFontPage.fromText(blockAttrs));
// case 'chars': charCount = Std.parseInt(blockAttrs.split("=").pop());
case 'char': chars.push(BMFontChar.fromText(blockAttrs));
// case 'kernings': kerningCount = Std.parseInt(blockAttrs.split("=").pop());
case 'kerning': kernings.push(BMFontKerning.fromText(blockAttrs));
}
}

return new BMFont(info, common, pages, chars, kernings.length > 0 ? kernings : null);
}

/**
* @see https://www.angelcode.com/products/bmfont/doc/file_format.html#bin
*/
public static function fromBytes(bytes:Bytes)
{
final bytes = new BytesInput(bytes);
final expectedBytes = [66, 77, 70]; // 'B', 'M', 'F'
for (b in expectedBytes)
{
var testByte = bytes.readByte();
if (testByte != b)
throw 'Invalid binary .fnt file. Found $testByte, expected $b';
}
var version = bytes.readByte();
if (version < 3)
{
FlxG.log.warn('The BMFont parser is made to work on files with version 3. Using earlier versions can cause issues!');
}

var info:BMFontInfo = null;
var common:BMFontCommon = null;
var pages:Array<BMFontPage> = null;
var chars:Array<BMFontChar> = null;
var kerning:Array<BMFontKerning> = null;

// parsing blocks
while (bytes.position < bytes.length)
{
final blockId:BMFontBlockId = bytes.readByte();
switch blockId
{
case INFO: info = BMFontInfo.fromBytes(bytes);
case COMMON: common = BMFontCommon.fromBytes(bytes);
case PAGES: pages = BMFontPage.listFromBytes(bytes);
case CHARS: chars = BMFontChar.listFromBytes(bytes);
case KERNING: kerning = BMFontKerning.listFromBytes(bytes);
}
}
return new BMFont(info, common, pages, chars, kerning);
}

public static function parse(data:FlxAngelCodeAsset):BMFont
{
return switch guessType(data)
{
case TEXT(text): fromText(text);
case XML(xml): fromXml(xml);
case BINARY(bytes): fromBytes(bytes);
};
}

/**
* A helper function that helps determine the type of BMFont descriptor from either the path or content of the file
* @param data The file path or the file content
* @return BMFontFileType
*/
static function guessType(data:FlxAngelCodeAsset):BMFontFileType
{
if (data is Xml)
{
return XML(cast(data, Xml).firstElement());
}

if (data is Bytes)
{
final bytes:Bytes = cast data;
if (isValidBytes(bytes))
return BINARY(bytes);

return detectFromText(bytes.toString());
}

if (data is String)
{
final dataStr:String = cast data;
if (Assets.exists(dataStr))
{
// dataStr is a file path
final bytes = Assets.getBytes(dataStr);
if(bytes == null)
return detectFromText(Assets.getText(dataStr));

if (isValidBytes(bytes))
return BINARY(bytes);

return detectFromText(bytes.toString());
}
else
{
// dataStr is the content of a file as a string (can be xml or just plain text)
return detectFromText(dataStr);
}
}

throw 'Invalid FlxAngelCodeAsset: $data';
}

static function safeParseXML(str:String)
{
// for js, Xml.parse throws if str is not valid XML but on desktop it just returns null
// This function will always return null if xml is invalid
try
{
var xml = Xml.parse(str);
return xml;
}
catch (e:Dynamic)
{
return null;
}
}

static function detectFromText(text:String):BMFontFileType
{
final xml = safeParseXML(text);
if (xml != null && xml.firstElement() != null)
{
return XML(xml.firstElement());
}
return TEXT(text);
}

static function isValidBytes(bytes:Bytes)
{
final expected = [66, 77, 70]; // 'B', 'M', 'F'
for (i in 0...expected.length)
{
if (bytes.get(i) != expected[i])
return false;
}

return true;
}
}

private enum abstract BMFontBlockId(Int) from Int
{
var INFO = 1;
var COMMON = 2;
var PAGES = 3;
var CHARS = 4;
var KERNING = 5;
}

private enum BMFontFileType
{
TEXT(text:String);
XML(xml:Xml);
BINARY(bytes:Bytes);
}
Loading

0 comments on commit f68d033

Please sign in to comment.