blob: e7e85a3ef31598491eb7770621f2cab07d827472 [file] [log] [blame]
import 'package:archive/archive.dart';
import 'package:xml/xml.dart';
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};
XmlDocument doc;
if (fnt.startsWith('<font>')) {
doc = parse(fnt);
if (doc == null) {
throw new ImageException('Invalid font XML');
}
} else {
doc = _parseTextFnt(fnt);
}
_parseFnt(doc, 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 = 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 = String.fromCharCodes(font_file.content as List<int>);
XmlDocument xml;
if (font_str.startsWith('<font>')) {
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<XmlElement> _childElements(XmlNode n) => n.children
.where((c) => c is XmlElement)
.map((c) => c as XmlElement);
void _parseFnt(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 (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 (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 = PngDecoder().decodeImage(imageFile.content as List<int>);
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 = 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);
}
}
}
}
}
}
XmlDocument _parseTextFnt(String content) {
var children = <XmlNode>[];
var pageList = <XmlNode>[];
var charList = <XmlNode>[];
var kerningList = <XmlNode>[];
List<XmlAttribute> charsAttrs;
List<XmlAttribute> 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 = XmlElement(new XmlName('info'), attrs, []);
children.add(info);
break;
case 'common':
var attrs = _parseParameters(tk);
var node = XmlElement(new XmlName('common'), attrs, []);
children.add(node);
break;
case 'page':
var attrs = _parseParameters(tk);
var page = XmlElement(new XmlName('page'), attrs, []);
pageList.add(page);
break;
case 'chars':
charsAttrs = _parseParameters(tk);
break;
case 'char':
var attrs = _parseParameters(tk);
var node = XmlElement(new XmlName('char'), attrs, []);
charList.add(node);
break;
case 'kernings':
kerningsAttrs = _parseParameters(tk);
break;
case 'kerning':
var attrs = _parseParameters(tk);
var node = XmlElement(new XmlName('kerning'), attrs, []);
kerningList.add(node);
break;
}
}
if (charsAttrs != null || charList.isNotEmpty) {
var node = XmlElement(new XmlName('chars'), charsAttrs, charList);
children.add(node);
}
if (kerningsAttrs != null || kerningList.isNotEmpty) {
var node = XmlElement(
new XmlName('kernings'), kerningsAttrs, kerningList);
children.add(node);
}
if (pageList.isNotEmpty) {
var pages = XmlElement(new XmlName('pages'), [], pageList);
children.add(pages);
}
var xml = XmlElement(new XmlName('font'), [], children);
var doc = XmlDocument([xml]);
return doc;
}
List<XmlAttribute> _parseParameters(List<String> tk) {
var params = <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 = XmlAttribute(new 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 = Image(width, height);
String toString() {
Map<String, int> x = {
'id': id,
'width': width,
'height': height,
'xoffset': xoffset,
'yoffset': yoffset,
'xadvance': xadvance,
'page': page,
'channel': channel
};
return 'Character $x';
}
}