blob: 213f480915e83c1716ae56a693f88f1bff3f9e47 [file] [log] [blame]
import 'package:archive/archive.dart';
import 'package:xml/xml.dart' as XML;
import 'image.dart';
import 'image_exception.dart';
import 'formats/png_decoder.dart';
/**
* Decode a [BitmapFont] from the contents of a zip file that stores the
* .fnt font definition and associated PNG images.
*/
BitmapFont readFontZip(List<int> bytes) =>
new BitmapFont.fromZip(bytes);
/**
* Decode a [BitmapFont] from the contents of [font] definition (.fnt) file,
* and an [Image] that stores the font [map].
*/
BitmapFont readFont(String font, Image map) =>
new BitmapFont.fromFnt(font, map);
/**
* A bitmap font that can be used with [drawString] and [drawChar] functions.
* You can generate a font files from a program
* like: http://kvazars.com/littera
*/
class BitmapFont {
String face = '';
int size = 0;
bool bold = false;
bool italic = false;
String charset = '';
String unicode = '';
int stretchH = 0;
bool smooth = false;
bool antialias = false;
List<int> padding = [];
List<int> spacing = [];
bool outline = false;
int lineHeight = 0;
int base = 0;
num scaleW = 0;
num scaleH = 0;
int pages = 0;
bool packed = false;
Map<int, BitmapFontCharacter> characters = {};
Map<int, Map<int, int>> kernings = {};
/**
* Decode a [BitmapFont] from the contents of [font] definition (.fnt) file,
* and an [Image] that stores the font [map].
*/
BitmapFont.fromFnt(String fnt, Image page) {
Map<int, Image> fontPages = { 0: page };
XML.XmlDocument xml;
if (fnt.startsWith('<font>')) {
xml = XML.parse(fnt);
if (xml == null) {
throw new ImageException('Invalid font XML');
}
} else {
xml = _parseTextFnt(fnt);
}
_parseFnt(xml, fontPages);
}
/**
* Decode a [BitmapFont] from the contents of a zip file that stores the
* .fnt font definition and associated PNG images.
*/
BitmapFont.fromZip(List<int> fileData) {
Archive arc = new ZipDecoder().decodeBytes(fileData);
ArchiveFile font_file;
for (int i = 0; i < arc.numberOfFiles(); ++i) {
if (arc.fileName(i).endsWith('.fnt')) {
font_file = arc.files[i];
break;
}
}
if (font_file == null) {
throw new ImageException('Invalid font archive');
}
String font_str = new String.fromCharCodes(font_file.content);
XML.XmlDocument xml;
if (font_str.startsWith('<font>')) {
xml = XML.parse(font_str);
if (xml == null) {
throw new ImageException('Invalid font XML');
}
} else {
xml = _parseTextFnt(font_str);
}
_parseFnt(xml, {}, arc);
}
/**
* Get the amount the writer x position should advance after drawing the
* character [ch].
*/
int characterXAdvance(String ch) {
if (ch.isEmpty) {
return 0;
}
int c = ch.codeUnits[0];
if (!characters.containsKey(ch)) {
return base ~/ 2;
}
return characters[c].xadvance;
}
Iterable<XML.XmlElement> _childElements(XML.XmlNode n) =>
n.children.where((c) => c is XML.XmlElement).map((c) => c as XML.XmlElement);
void _parseFnt(XML.XmlDocument xml, Map<int, Image> fontPages,
[Archive arc]) {
if (xml.children.length != 1) {
throw new ImageException('Invalid font XML');
}
var font = xml.children[0];
for (var c in _childElements(font)) {
String name = c.name.toString();
if (name == 'info') {
for (XML.XmlAttribute a in c.attributes) {
switch (a.name.toString()) {
case 'face':
face = a.value;
break;
case 'size':
size = int.parse(a.value);
break;
case 'bold':
bold = (int.parse(a.value) == 1);
break;
case 'italic':
italic = (int.parse(a.value) == 1);
break;
case 'charset':
charset = a.value;
break;
case 'unicode':
unicode = a.value;
break;
case 'stretchH':
stretchH = int.parse(a.value);
break;
case 'smooth':
smooth = (int.parse(a.value) == 1);
break;
case 'antialias':
antialias = (int.parse(a.value) == 1);
break;
case 'padding':
List<String> tk = a.value.split(',');
padding = [];
for (String t in tk) {
padding.add(int.parse(t));
}
break;
case 'spacing':
List<String> tk = a.value.split(',');
spacing = [];
for (String t in tk) {
spacing.add(int.parse(t));
}
break;
case 'outline':
outline = (int.parse(a.value) == 1);
break;
}
}
} else if (name == 'common') {
for (XML.XmlAttribute a in c.attributes) {
switch (a.name.toString()) {
case 'lineHeight':
lineHeight = int.parse(a.value);
break;
case 'base':
base = int.parse(a.value);
break;
case 'scaleW':
scaleW = int.parse(a.value);
break;
case 'scaleH':
scaleH = int.parse(a.value);
break;
case 'pages':
pages = int.parse(a.value);
break;
case 'packed':
packed = (int.parse(a.value) == 1);
break;
}
}
} else if (name == 'pages') {
for (var page in _childElements(c)) {
int id = int.parse(page.getAttribute('id'));
String filename = page.getAttribute('file');
if (fontPages.containsKey(id)) {
throw new ImageException('Duplicate font page id found: $id.');
}
if (arc != null) {
ArchiveFile imageFile = _findFile(arc, filename);
if (imageFile == null) {
throw new ImageException('Font zip missing font page image '
'$filename');
}
Image image = new PngDecoder().decodeImage(imageFile.content);
fontPages[id] = image;
}
}
} else if (name == 'kernings') {
for (var kerning in _childElements(c)) {
int first = int.parse(kerning.getAttribute('first'));
int second = int.parse(kerning.getAttribute('second'));
int amount = int.parse(kerning.getAttribute('amount'));
if (!kernings.containsKey(first)) {
kernings[first] = {};
}
kernings[first][second] = amount;
}
}
}
for (var c in _childElements(font)) {
String name = c.name.toString();
if (name == 'chars') {
for (var char in _childElements(c)) {
int id = int.parse(char.getAttribute('id'));
int x = int.parse(char.getAttribute('x'));
int y = int.parse(char.getAttribute('y'));
int width = int.parse(char.getAttribute('width'));
int height = int.parse(char.getAttribute('height'));
int xoffset = int.parse(char.getAttribute('xoffset'));
int yoffset = int.parse(char.getAttribute('yoffset'));
int xadvance = int.parse(char.getAttribute('xadvance'));
int page = int.parse(char.getAttribute('page'));
int chnl = int.parse(char.getAttribute('chnl'));
if (!fontPages.containsKey(page)) {
throw new ImageException('Missing page image: $page');
}
Image fontImage = fontPages[page];
BitmapFontCharacter ch = new BitmapFontCharacter(id, width, height,
xoffset, yoffset, xadvance, page, chnl);
characters[id] = ch;
int x2 = x + width;
int y2 = y + height;
int pi = 0;
Image image = ch.image;
for (int yi = y; yi < y2; ++yi) {
for (int xi = x; xi < x2; ++xi) {
image[pi++] = fontImage.getPixel(xi, yi);
}
}
}
}
}
}
XML.XmlDocument _parseTextFnt(String content) {
var children = <XML.XmlNode>[];
var pageList = <XML.XmlNode>[];
var charList = <XML.XmlNode>[];
var kerningList = <XML.XmlNode>[];
var charsAttrs;
var kerningsAttrs;
List<String> lines = content.split('\n');
for (String line in lines) {
if (line.isEmpty) {
continue;
}
List<String> tk = line.split(' ');
switch (tk[0]) {
case 'info':
var attrs = _parseParameters(tk);
var info = new XML.XmlElement(new XML.XmlName('info'), attrs, []);
children.add(info);
break;
case 'common':
var attrs = _parseParameters(tk);
var node = new XML.XmlElement(new XML.XmlName('common'), attrs, []);
children.add(node);
break;
case 'page':
var attrs = _parseParameters(tk);
var page = new XML.XmlElement(new XML.XmlName('page'), attrs, []);
pageList.add(page);
break;
case 'chars':
charsAttrs = _parseParameters(tk);
break;
case 'char':
var attrs = _parseParameters(tk);
var node = new XML.XmlElement(new XML.XmlName('char'), attrs, []);
charList.add(node);
break;
case 'kernings':
kerningsAttrs = _parseParameters(tk);
break;
case 'kerning':
var attrs = _parseParameters(tk);
var node = new XML.XmlElement(new XML.XmlName('kerning'), attrs, []);
kerningList.add(node);
break;
}
}
if (charsAttrs != null || charList.isNotEmpty) {
var node = new XML.XmlElement(new XML.XmlName('chars'), charsAttrs,
charList);
children.add(node);
}
if (kerningsAttrs != null || kerningList.isNotEmpty) {
var node = new XML.XmlElement(new XML.XmlName('kernings'), kerningsAttrs,
kerningList);
children.add(node);
}
if (pageList.isNotEmpty) {
var pages = new XML.XmlElement(new XML.XmlName('pages'), [], pageList);
children.add(pages);
}
var xml = new XML.XmlElement(new XML.XmlName('font'), [], children);
var doc = new XML.XmlDocument([xml]);
return doc;
}
List<XML.XmlAttribute> _parseParameters(List<String> tk) {
var params = <XML.XmlAttribute>[];
for (int ti = 1; ti < tk.length; ++ti) {
if (tk[ti].isEmpty) {
continue;
}
List<String> atk = tk[ti].split('=');
if (atk.length != 2) {
continue;
}
// Remove all " characters
atk[1] = atk[1].replaceAll('"', '');
var a = new XML.XmlAttribute(new XML.XmlName(atk[0]), atk[1]);
params.add(a);
}
return params;
}
static ArchiveFile _findFile(Archive arc, String filename) {
for (ArchiveFile f in arc.files) {
if (f.name == filename) {
return f;
}
}
return null;
}
}
/**
* A single character in a [BitmapFont].
*/
class BitmapFontCharacter {
final int id;
final int width;
final int height;
final int xoffset;
final int yoffset;
final int xadvance;
final int page;
final int channel;
final Image image;
BitmapFontCharacter(this.id, int width, int height,
this.xoffset, this.yoffset, this.xadvance, this.page,
this.channel) :
this.width = width,
this.height = height,
image = new Image(width, height);
String toString() {
Map x = {'id': id, 'width': width, 'height': height, 'xoffset': xoffset,
'yoffset': yoffset, 'xadvance': xadvance, 'page': page,
'channel': channel};
return 'Character $x';
}
}