@flighthq/bitmapfont-formats 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitmapFontFnt.d.ts +4 -0
- package/dist/bitmapFontFnt.d.ts.map +1 -0
- package/dist/bitmapFontFnt.js +141 -0
- package/dist/bitmapFontFnt.js.map +1 -0
- package/dist/bitmapFontJson.d.ts +3 -0
- package/dist/bitmapFontJson.d.ts.map +1 -0
- package/dist/bitmapFontJson.js +111 -0
- package/dist/bitmapFontJson.js.map +1 -0
- package/dist/bitmapFontRecord.d.ts +31 -0
- package/dist/bitmapFontRecord.d.ts.map +1 -0
- package/dist/bitmapFontRecord.js +71 -0
- package/dist/bitmapFontRecord.js.map +1 -0
- package/dist/bitmapFontXml.d.ts +3 -0
- package/dist/bitmapFontXml.d.ts.map +1 -0
- package/dist/bitmapFontXml.js +97 -0
- package/dist/bitmapFontXml.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
- package/src/bitmapFontFnt.test.ts +146 -0
- package/src/bitmapFontJson.test.ts +128 -0
- package/src/bitmapFontRecord.test.ts +121 -0
- package/src/bitmapFontXml.test.ts +70 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BitmapFont, BitmapFontParseOptions } from '@flighthq/types';
|
|
2
|
+
export declare function formatBitmapFontFnt(font: Readonly<BitmapFont>): string;
|
|
3
|
+
export declare function parseBitmapFontFnt(text: string, options?: Readonly<BitmapFontParseOptions>): BitmapFont | null;
|
|
4
|
+
//# sourceMappingURL=bitmapFontFnt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontFnt.d.ts","sourceRoot":"","sources":["../src/bitmapFontFnt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAc,MAAM,iBAAiB,CAAC;AAkBtF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,CAwCtE;AAOD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,UAAU,GAAG,IAAI,CAI9G"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { getBitmapFontMetrics } from '@flighthq/bitmapfont';
|
|
2
|
+
import { buildBitmapFontFromRecord } from './bitmapFontRecord';
|
|
3
|
+
// Re-emits a `BitmapFont` as the classic AngelCode/BMFont text `.fnt` (`info`/`common`/`page`/`char`/
|
|
4
|
+
// `kerning` lines of `key=value` pairs). Lossless for the fields the model carries — the glyph table
|
|
5
|
+
// (including each glyph's `page`), kerning pairs, encoding, and line metrics all round-trip
|
|
6
|
+
// (`parseBitmapFontFnt` of this output rebuilds an equivalent font). A `page` line is emitted per
|
|
7
|
+
// font page; the page references cannot round-trip to filenames: a `BitmapFont` holds live
|
|
8
|
+
// `TextureAtlas` pages, not the source page paths, so each `page` line carries an empty `file=""` and
|
|
9
|
+
// the reader supplies the atlases again through its `resolvePage`. `info`/`common` beyond the line
|
|
10
|
+
// metrics (face, style, spacing, packing) are not part of the model and are emitted as neutral defaults.
|
|
11
|
+
export function formatBitmapFontFnt(font) {
|
|
12
|
+
const metrics = getBitmapFontMetrics(font);
|
|
13
|
+
const lineHeight = metrics.ascent + metrics.descent + metrics.lineGap;
|
|
14
|
+
const base = metrics.ascent;
|
|
15
|
+
const primaryImage = font.pages[0]?.image ?? null;
|
|
16
|
+
const scaleW = primaryImage !== null ? primaryImage.width : 0;
|
|
17
|
+
const scaleH = primaryImage !== null ? primaryImage.height : 0;
|
|
18
|
+
const pageCount = Math.max(font.pages.length, 1);
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push(`info face="" size=${lineHeight} bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=0,0 outline=0`);
|
|
21
|
+
lines.push(`common lineHeight=${lineHeight} base=${base} scaleW=${scaleW} scaleH=${scaleH} pages=${pageCount} packed=0 alphaChnl=1 redChnl=0 greenChnl=0 blueChnl=0`);
|
|
22
|
+
for (let id = 0; id < pageCount; id++) {
|
|
23
|
+
lines.push(`page id=${id} file=""`);
|
|
24
|
+
}
|
|
25
|
+
const codepoints = [...font.glyphs.keys()];
|
|
26
|
+
lines.push(`chars count=${codepoints.length}`);
|
|
27
|
+
for (const codepoint of codepoints) {
|
|
28
|
+
const glyph = font.glyphs.get(codepoint);
|
|
29
|
+
lines.push(`char id=${codepoint} x=${glyph.x} y=${glyph.y} width=${glyph.width} height=${glyph.height} ` +
|
|
30
|
+
`xoffset=${glyph.bearingX} yoffset=${base - glyph.bearingY} xadvance=${glyph.advance} page=${glyph.page} chnl=15`);
|
|
31
|
+
}
|
|
32
|
+
const kernKeys = [...font.kerning.keys()];
|
|
33
|
+
lines.push(`kernings count=${kernKeys.length}`);
|
|
34
|
+
for (const key of kernKeys) {
|
|
35
|
+
const amount = font.kerning.get(key);
|
|
36
|
+
const first = key >>> 16;
|
|
37
|
+
const second = key & 0xffff;
|
|
38
|
+
lines.push(`kerning first=${first} second=${second} amount=${amount}`);
|
|
39
|
+
}
|
|
40
|
+
return lines.join('\n') + '\n';
|
|
41
|
+
}
|
|
42
|
+
// Parses the classic AngelCode/BMFont text `.fnt` into a `BitmapFont`. The format is one record per
|
|
43
|
+
// line: a leading tag (`info`/`common`/`page`/`char`/`kerning`) followed by `key=value` pairs whose
|
|
44
|
+
// values are bare tokens, quoted strings, or comma lists. The atlas page named on the `page` line is
|
|
45
|
+
// rehydrated through `options.resolvePage`. Returns the `null` sentinel — never throwing — when the
|
|
46
|
+
// text carries no `common` line metrics, no `char` records, or its atlas page cannot be resolved.
|
|
47
|
+
export function parseBitmapFontFnt(text, options) {
|
|
48
|
+
const record = parseBitmapFontFntRecord(text);
|
|
49
|
+
if (record === null)
|
|
50
|
+
return null;
|
|
51
|
+
return buildBitmapFontFromRecord(record, options);
|
|
52
|
+
}
|
|
53
|
+
// Parses the text `.fnt` into the neutral record, or `null` when required blocks are absent or
|
|
54
|
+
// malformed. Kept separate from `buildBitmapFontFromRecord` so the atlas-resolution step is shared
|
|
55
|
+
// across the text/XML/JSON front-ends.
|
|
56
|
+
function parseBitmapFontFntRecord(text) {
|
|
57
|
+
let lineHeight = null;
|
|
58
|
+
let base = null;
|
|
59
|
+
const pages = [];
|
|
60
|
+
const chars = [];
|
|
61
|
+
const kernings = [];
|
|
62
|
+
for (const rawLine of text.split(/\r\n?|\n/)) {
|
|
63
|
+
const line = rawLine.trim();
|
|
64
|
+
if (line === '')
|
|
65
|
+
continue;
|
|
66
|
+
const spaceAt = line.search(/\s/);
|
|
67
|
+
const tag = spaceAt < 0 ? line : line.slice(0, spaceAt);
|
|
68
|
+
const fields = parseFntFields(line.slice(tag.length));
|
|
69
|
+
if (tag === 'common') {
|
|
70
|
+
lineHeight = readFntNumber(fields.lineHeight);
|
|
71
|
+
base = readFntNumber(fields.base);
|
|
72
|
+
}
|
|
73
|
+
else if (tag === 'page') {
|
|
74
|
+
const id = readFntNumber(fields.id);
|
|
75
|
+
if (id !== null)
|
|
76
|
+
pages.push({ file: fields.file ?? '', id });
|
|
77
|
+
}
|
|
78
|
+
else if (tag === 'char') {
|
|
79
|
+
const char = readFntChar(fields);
|
|
80
|
+
if (char !== null)
|
|
81
|
+
chars.push(char);
|
|
82
|
+
}
|
|
83
|
+
else if (tag === 'kerning') {
|
|
84
|
+
const kerning = readFntKerning(fields);
|
|
85
|
+
if (kerning !== null)
|
|
86
|
+
kernings.push(kerning);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (lineHeight === null || base === null || chars.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return { base, chars, encoding: 'raster', kernings, lineHeight, pages };
|
|
92
|
+
}
|
|
93
|
+
// Tokenizes a record's `key=value` pairs. Values may be double-quoted (holding spaces), or a bare
|
|
94
|
+
// non-space run (numbers, comma lists). Quotes are stripped; the raw string value is stored.
|
|
95
|
+
function parseFntFields(rest) {
|
|
96
|
+
const fields = {};
|
|
97
|
+
const re = /([A-Za-z_]\w*)\s*=\s*(?:"([^"]*)"|(\S+))/g;
|
|
98
|
+
let match;
|
|
99
|
+
while ((match = re.exec(rest)) !== null) {
|
|
100
|
+
fields[match[1]] = match[2] !== undefined ? match[2] : (match[3] ?? '');
|
|
101
|
+
}
|
|
102
|
+
return fields;
|
|
103
|
+
}
|
|
104
|
+
function readFntChar(fields) {
|
|
105
|
+
const id = readFntNumber(fields.id);
|
|
106
|
+
const x = readFntNumber(fields.x);
|
|
107
|
+
const y = readFntNumber(fields.y);
|
|
108
|
+
const width = readFntNumber(fields.width);
|
|
109
|
+
const height = readFntNumber(fields.height);
|
|
110
|
+
const xoffset = readFntNumber(fields.xoffset);
|
|
111
|
+
const yoffset = readFntNumber(fields.yoffset);
|
|
112
|
+
const xadvance = readFntNumber(fields.xadvance);
|
|
113
|
+
if (id === null ||
|
|
114
|
+
x === null ||
|
|
115
|
+
y === null ||
|
|
116
|
+
width === null ||
|
|
117
|
+
height === null ||
|
|
118
|
+
xoffset === null ||
|
|
119
|
+
yoffset === null ||
|
|
120
|
+
xadvance === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return { height, id, page: readFntNumber(fields.page) ?? 0, width, x, xadvance, xoffset, y, yoffset };
|
|
124
|
+
}
|
|
125
|
+
function readFntKerning(fields) {
|
|
126
|
+
const first = readFntNumber(fields.first);
|
|
127
|
+
const second = readFntNumber(fields.second);
|
|
128
|
+
const amount = readFntNumber(fields.amount);
|
|
129
|
+
if (first === null || second === null || amount === null)
|
|
130
|
+
return null;
|
|
131
|
+
return { amount, first, second };
|
|
132
|
+
}
|
|
133
|
+
// Parses a `.fnt` field value as a finite number, returning `null` (the malformed sentinel) for an
|
|
134
|
+
// absent field or a non-numeric value.
|
|
135
|
+
function readFntNumber(value) {
|
|
136
|
+
if (value === undefined || value.trim() === '')
|
|
137
|
+
return null;
|
|
138
|
+
const parsed = Number(value);
|
|
139
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=bitmapFontFnt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontFnt.js","sourceRoot":"","sources":["../src/bitmapFontFnt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontJson.d.ts","sourceRoot":"","sources":["../src/bitmapFontJson.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAsB,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAiB9F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,UAAU,GAAG,IAAI,CAI/G"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { buildBitmapFontFromRecord } from './bitmapFontRecord';
|
|
2
|
+
// Parses the JSON AngelCode/BMFont export into a `BitmapFont`. The shape mirrors the text/XML blocks as
|
|
3
|
+
// an object: `{ common: { lineHeight, base }, pages: [file, …], chars: [{ id, x, … }], kernings: [{
|
|
4
|
+
// first, second, amount }] }`, where `pages` is a filename array whose index is the page id. A
|
|
5
|
+
// distance-field export's `distanceField.fieldType` (`sdf`/`msdf`) selects the font encoding; classic
|
|
6
|
+
// exports stay `raster`. Semantics otherwise match `parseBitmapFontFnt`, including `resolvePage` atlas
|
|
7
|
+
// rehydration. Returns the `null` sentinel — never throwing — for malformed JSON, a missing
|
|
8
|
+
// `common`/`chars`, or an atlas page that cannot be resolved.
|
|
9
|
+
export function parseBitmapFontJson(text, options) {
|
|
10
|
+
const record = parseBitmapFontJsonRecord(text);
|
|
11
|
+
if (record === null)
|
|
12
|
+
return null;
|
|
13
|
+
return buildBitmapFontFromRecord(record, options);
|
|
14
|
+
}
|
|
15
|
+
// Parses the JSON export into the neutral record, or `null` when the JSON is malformed or the
|
|
16
|
+
// `common`/`chars` blocks are absent or malformed.
|
|
17
|
+
function parseBitmapFontJsonRecord(text) {
|
|
18
|
+
let root;
|
|
19
|
+
try {
|
|
20
|
+
root = JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!isObject(root))
|
|
26
|
+
return null;
|
|
27
|
+
const common = root.common;
|
|
28
|
+
if (!isObject(common))
|
|
29
|
+
return null;
|
|
30
|
+
const lineHeight = readJsonNumber(common.lineHeight);
|
|
31
|
+
const base = readJsonNumber(common.base);
|
|
32
|
+
if (lineHeight === null || base === null)
|
|
33
|
+
return null;
|
|
34
|
+
const rawChars = root.chars;
|
|
35
|
+
if (!Array.isArray(rawChars))
|
|
36
|
+
return null;
|
|
37
|
+
const chars = [];
|
|
38
|
+
for (const raw of rawChars) {
|
|
39
|
+
const char = readJsonChar(raw);
|
|
40
|
+
if (char !== null)
|
|
41
|
+
chars.push(char);
|
|
42
|
+
}
|
|
43
|
+
if (chars.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const pages = [];
|
|
46
|
+
if (Array.isArray(root.pages)) {
|
|
47
|
+
for (let id = 0; id < root.pages.length; id++) {
|
|
48
|
+
const file = root.pages[id];
|
|
49
|
+
pages.push({ file: typeof file === 'string' ? file : '', id });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const kernings = [];
|
|
53
|
+
if (Array.isArray(root.kernings)) {
|
|
54
|
+
for (const raw of root.kernings) {
|
|
55
|
+
const kerning = readJsonKerning(raw);
|
|
56
|
+
if (kerning !== null)
|
|
57
|
+
kernings.push(kerning);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { base, chars, encoding: readJsonEncoding(root.distanceField), kernings, lineHeight, pages };
|
|
61
|
+
}
|
|
62
|
+
function isObject(value) {
|
|
63
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
function readJsonChar(raw) {
|
|
66
|
+
if (!isObject(raw))
|
|
67
|
+
return null;
|
|
68
|
+
const id = readJsonNumber(raw.id);
|
|
69
|
+
const x = readJsonNumber(raw.x);
|
|
70
|
+
const y = readJsonNumber(raw.y);
|
|
71
|
+
const width = readJsonNumber(raw.width);
|
|
72
|
+
const height = readJsonNumber(raw.height);
|
|
73
|
+
const xoffset = readJsonNumber(raw.xoffset);
|
|
74
|
+
const yoffset = readJsonNumber(raw.yoffset);
|
|
75
|
+
const xadvance = readJsonNumber(raw.xadvance);
|
|
76
|
+
if (id === null ||
|
|
77
|
+
x === null ||
|
|
78
|
+
y === null ||
|
|
79
|
+
width === null ||
|
|
80
|
+
height === null ||
|
|
81
|
+
xoffset === null ||
|
|
82
|
+
yoffset === null ||
|
|
83
|
+
xadvance === null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { height, id, page: readJsonNumber(raw.page) ?? 0, width, x, xadvance, xoffset, y, yoffset };
|
|
87
|
+
}
|
|
88
|
+
// Reads the encoding from a `distanceField` block: `msdf`/`sdf` when its `fieldType` names one, else
|
|
89
|
+
// `raster` (classic pre-rendered BMFont, and any unrecognized field type).
|
|
90
|
+
function readJsonEncoding(distanceField) {
|
|
91
|
+
if (isObject(distanceField)) {
|
|
92
|
+
const fieldType = distanceField.fieldType;
|
|
93
|
+
if (fieldType === 'msdf' || fieldType === 'sdf')
|
|
94
|
+
return fieldType;
|
|
95
|
+
}
|
|
96
|
+
return 'raster';
|
|
97
|
+
}
|
|
98
|
+
function readJsonKerning(raw) {
|
|
99
|
+
if (!isObject(raw))
|
|
100
|
+
return null;
|
|
101
|
+
const first = readJsonNumber(raw.first);
|
|
102
|
+
const second = readJsonNumber(raw.second);
|
|
103
|
+
const amount = readJsonNumber(raw.amount);
|
|
104
|
+
if (first === null || second === null || amount === null)
|
|
105
|
+
return null;
|
|
106
|
+
return { amount, first, second };
|
|
107
|
+
}
|
|
108
|
+
function readJsonNumber(value) {
|
|
109
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=bitmapFontJson.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontJson.js","sourceRoot":"","sources":["../src/bitmapFontJson.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BitmapFont, BitmapFontEncoding, BitmapFontParseOptions } from '@flighthq/types';
|
|
2
|
+
export interface BitmapFontCharRecord {
|
|
3
|
+
height: number;
|
|
4
|
+
id: number;
|
|
5
|
+
page: number;
|
|
6
|
+
width: number;
|
|
7
|
+
x: number;
|
|
8
|
+
xadvance: number;
|
|
9
|
+
xoffset: number;
|
|
10
|
+
y: number;
|
|
11
|
+
yoffset: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BitmapFontKerningRecord {
|
|
14
|
+
amount: number;
|
|
15
|
+
first: number;
|
|
16
|
+
second: number;
|
|
17
|
+
}
|
|
18
|
+
export interface BitmapFontPageRecord {
|
|
19
|
+
file: string;
|
|
20
|
+
id: number;
|
|
21
|
+
}
|
|
22
|
+
export interface BitmapFontRecord {
|
|
23
|
+
base: number;
|
|
24
|
+
chars: BitmapFontCharRecord[];
|
|
25
|
+
encoding: BitmapFontEncoding;
|
|
26
|
+
kernings: BitmapFontKerningRecord[];
|
|
27
|
+
lineHeight: number;
|
|
28
|
+
pages: BitmapFontPageRecord[];
|
|
29
|
+
}
|
|
30
|
+
export declare function buildBitmapFontFromRecord(record: Readonly<BitmapFontRecord>, options?: Readonly<BitmapFontParseOptions>): BitmapFont | null;
|
|
31
|
+
//# sourceMappingURL=bitmapFontRecord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontRecord.d.ts","sourceRoot":"","sources":["../src/bitmapFontRecord.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EAEV,kBAAkB,EAGlB,sBAAsB,EAEvB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,MAAM,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAOD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,uBAAuB,EAAE,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,oBAAoB,EAAE,CAAC;CAC/B;AAaD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAClC,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GACzC,UAAU,GAAG,IAAI,CAyDnB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createBitmapFont } from '@flighthq/bitmapfont';
|
|
2
|
+
// Maps a parsed `BitmapFontRecord` onto a `BitmapFont` via `createBitmapFont`, resolving every atlas
|
|
3
|
+
// page the record declares through `options.resolvePage` (called once per page id). The resolved
|
|
4
|
+
// atlases become the font's page-indexed `pages` (page id = index), and each `char`'s `page` carries
|
|
5
|
+
// through to its glyph. Resolution rule: a page a glyph actually samples must resolve — if any
|
|
6
|
+
// referenced page's atlas is `null` (including when `resolvePage` is omitted, so nothing resolves),
|
|
7
|
+
// this returns the `null` sentinel and the parse fails as a whole; a declared-but-unreferenced page
|
|
8
|
+
// that fails to resolve is tolerated and simply left absent. Each `char` becomes a glyph
|
|
9
|
+
// (`xoffset`/`yoffset` → `bearingX`/`bearingY`, `xadvance` → `advance`, `page` → `page`); each
|
|
10
|
+
// `kerning` becomes a pair; the line metrics derive from the BMFont `common` fields (`ascent` =
|
|
11
|
+
// `base`, `descent` = `lineHeight - base`, `lineGap` = 0), which `formatBitmapFontFnt` inverts
|
|
12
|
+
// losslessly.
|
|
13
|
+
export function buildBitmapFontFromRecord(record, options) {
|
|
14
|
+
const resolvePage = options?.resolvePage;
|
|
15
|
+
const resolved = new Map();
|
|
16
|
+
let maxPageId = -1;
|
|
17
|
+
if (resolvePage !== undefined) {
|
|
18
|
+
for (const page of record.pages) {
|
|
19
|
+
const atlas = resolvePage(page.id, page.file);
|
|
20
|
+
if (atlas !== null) {
|
|
21
|
+
resolved.set(page.id, atlas);
|
|
22
|
+
if (page.id > maxPageId)
|
|
23
|
+
maxPageId = page.id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Every page a glyph samples must have resolved; an unresolved referenced page collapses the parse.
|
|
28
|
+
for (const char of record.chars) {
|
|
29
|
+
if (!resolved.has(char.page))
|
|
30
|
+
return null;
|
|
31
|
+
if (char.page > maxPageId)
|
|
32
|
+
maxPageId = char.page;
|
|
33
|
+
}
|
|
34
|
+
const pages = [];
|
|
35
|
+
for (let id = 0; id <= maxPageId; id++) {
|
|
36
|
+
const atlas = resolved.get(id);
|
|
37
|
+
// A hole only occurs for a declared-but-unreferenced page that failed to resolve; no glyph
|
|
38
|
+
// indexes it, so it is left absent (`getBitmapFontPage` reports null for that index).
|
|
39
|
+
if (atlas !== undefined)
|
|
40
|
+
pages[id] = atlas;
|
|
41
|
+
}
|
|
42
|
+
const glyphs = record.chars.map((char) => ({
|
|
43
|
+
advance: char.xadvance,
|
|
44
|
+
bearingX: char.xoffset,
|
|
45
|
+
// BMFont `yoffset` is the distance down from the line top to the glyph's top; `GlyphEntry.bearingY`
|
|
46
|
+
// is the distance up from the baseline to that same top (the convention the renderer and
|
|
47
|
+
// @flighthq/glyphatlas share, `quadY = baselineY - bearingY`). Convert across the two references
|
|
48
|
+
// via `base` (line-top-to-baseline). `formatBitmapFontFnt` inverts this with `yoffset = base - bearingY`.
|
|
49
|
+
bearingY: record.base - char.yoffset,
|
|
50
|
+
codepoint: char.id,
|
|
51
|
+
height: char.height,
|
|
52
|
+
page: char.page,
|
|
53
|
+
width: char.width,
|
|
54
|
+
x: char.x,
|
|
55
|
+
y: char.y,
|
|
56
|
+
}));
|
|
57
|
+
const kerning = record.kernings.map((pair) => ({
|
|
58
|
+
amount: pair.amount,
|
|
59
|
+
left: pair.first,
|
|
60
|
+
right: pair.second,
|
|
61
|
+
}));
|
|
62
|
+
const data = {
|
|
63
|
+
encoding: record.encoding,
|
|
64
|
+
glyphs,
|
|
65
|
+
kerning,
|
|
66
|
+
metrics: { ascent: record.base, descent: record.lineHeight - record.base, lineGap: 0 },
|
|
67
|
+
pages,
|
|
68
|
+
};
|
|
69
|
+
return createBitmapFont(data);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=bitmapFontRecord.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontRecord.js","sourceRoot":"","sources":["../src/bitmapFontRecord.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontXml.d.ts","sourceRoot":"","sources":["../src/bitmapFontXml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAuB1E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,UAAU,GAAG,IAAI,CAI9G"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getXmlElementAttribute, getXmlElementAttributeNumber, getXmlElementChildByName, getXmlElementChildrenByName, parseXmlDocument, } from '@flighthq/xml';
|
|
2
|
+
import { buildBitmapFontFromRecord } from './bitmapFontRecord';
|
|
3
|
+
// Parses the XML AngelCode/BMFont `.fnt` variant into a `BitmapFont`. The document is
|
|
4
|
+
// `<font><info/><common/><pages><page/></pages><chars><char/></chars><kernings><kerning/></kernings></font>`
|
|
5
|
+
// with every field an element attribute. Semantics match `parseBitmapFontFnt` — the same line metrics,
|
|
6
|
+
// glyph mapping, and `resolvePage` atlas rehydration. Returns the `null` sentinel — never throwing —
|
|
7
|
+
// for malformed XML, a missing `common`/`chars`, or an atlas page that cannot be resolved.
|
|
8
|
+
export function parseBitmapFontXml(text, options) {
|
|
9
|
+
const record = parseBitmapFontXmlRecord(text);
|
|
10
|
+
if (record === null)
|
|
11
|
+
return null;
|
|
12
|
+
return buildBitmapFontFromRecord(record, options);
|
|
13
|
+
}
|
|
14
|
+
// Parses the XML `.fnt` into the neutral record, or `null` when the root/`common`/`chars` blocks are
|
|
15
|
+
// absent or a required attribute is malformed.
|
|
16
|
+
function parseBitmapFontXmlRecord(text) {
|
|
17
|
+
const root = parseXmlDocument(text);
|
|
18
|
+
if (root === null || root.name !== 'font')
|
|
19
|
+
return null;
|
|
20
|
+
const common = getXmlElementChildByName(root, 'common');
|
|
21
|
+
if (common === null)
|
|
22
|
+
return null;
|
|
23
|
+
const lineHeight = getXmlElementAttributeNumber(common, 'lineHeight');
|
|
24
|
+
const base = getXmlElementAttributeNumber(common, 'base');
|
|
25
|
+
if (lineHeight === null || base === null)
|
|
26
|
+
return null;
|
|
27
|
+
const pages = [];
|
|
28
|
+
const pagesElement = getXmlElementChildByName(root, 'pages');
|
|
29
|
+
if (pagesElement !== null) {
|
|
30
|
+
for (const pageElement of getXmlElementChildrenByName(pagesElement, 'page')) {
|
|
31
|
+
const id = getXmlElementAttributeNumber(pageElement, 'id');
|
|
32
|
+
if (id !== null)
|
|
33
|
+
pages.push({ file: getXmlElementAttribute(pageElement, 'file') ?? '', id });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const chars = [];
|
|
37
|
+
const charsElement = getXmlElementChildByName(root, 'chars');
|
|
38
|
+
if (charsElement !== null) {
|
|
39
|
+
for (const charElement of getXmlElementChildrenByName(charsElement, 'char')) {
|
|
40
|
+
const char = readXmlChar(charElement);
|
|
41
|
+
if (char !== null)
|
|
42
|
+
chars.push(char);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (chars.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
const kernings = [];
|
|
48
|
+
const kerningsElement = getXmlElementChildByName(root, 'kernings');
|
|
49
|
+
if (kerningsElement !== null) {
|
|
50
|
+
for (const kerningElement of getXmlElementChildrenByName(kerningsElement, 'kerning')) {
|
|
51
|
+
const kerning = readXmlKerning(kerningElement);
|
|
52
|
+
if (kerning !== null)
|
|
53
|
+
kernings.push(kerning);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { base, chars, encoding: 'raster', kernings, lineHeight, pages };
|
|
57
|
+
}
|
|
58
|
+
function readXmlChar(element) {
|
|
59
|
+
const id = getXmlElementAttributeNumber(element, 'id');
|
|
60
|
+
const x = getXmlElementAttributeNumber(element, 'x');
|
|
61
|
+
const y = getXmlElementAttributeNumber(element, 'y');
|
|
62
|
+
const width = getXmlElementAttributeNumber(element, 'width');
|
|
63
|
+
const height = getXmlElementAttributeNumber(element, 'height');
|
|
64
|
+
const xoffset = getXmlElementAttributeNumber(element, 'xoffset');
|
|
65
|
+
const yoffset = getXmlElementAttributeNumber(element, 'yoffset');
|
|
66
|
+
const xadvance = getXmlElementAttributeNumber(element, 'xadvance');
|
|
67
|
+
if (id === null ||
|
|
68
|
+
x === null ||
|
|
69
|
+
y === null ||
|
|
70
|
+
width === null ||
|
|
71
|
+
height === null ||
|
|
72
|
+
xoffset === null ||
|
|
73
|
+
yoffset === null ||
|
|
74
|
+
xadvance === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
height,
|
|
79
|
+
id,
|
|
80
|
+
page: getXmlElementAttributeNumber(element, 'page') ?? 0,
|
|
81
|
+
width,
|
|
82
|
+
x,
|
|
83
|
+
xadvance,
|
|
84
|
+
xoffset,
|
|
85
|
+
y,
|
|
86
|
+
yoffset,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function readXmlKerning(element) {
|
|
90
|
+
const first = getXmlElementAttributeNumber(element, 'first');
|
|
91
|
+
const second = getXmlElementAttributeNumber(element, 'second');
|
|
92
|
+
const amount = getXmlElementAttributeNumber(element, 'amount');
|
|
93
|
+
if (first === null || second === null || amount === null)
|
|
94
|
+
return null;
|
|
95
|
+
return { amount, first, second };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=bitmapFontXml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapFontXml.js","sourceRoot":"","sources":["../src/bitmapFontXml.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/bitmapfont-formats",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/bitmapfont": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0",
|
|
32
|
+
"@flighthq/xml": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@flighthq/textureatlas": "*",
|
|
36
|
+
"typescript": "^5.3.0"
|
|
37
|
+
},
|
|
38
|
+
"description": "AngelCode/BMFont codec neighbor of @flighthq/bitmapfont — parse the text/XML/JSON .fnt variants into a BitmapFont and re-emit the text form",
|
|
39
|
+
"sideEffects": false
|
|
40
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGlyphSourceFromBitmapFont,
|
|
3
|
+
getBitmapFontGlyph,
|
|
4
|
+
getBitmapFontKerning,
|
|
5
|
+
getBitmapFontMetrics,
|
|
6
|
+
getBitmapFontPage,
|
|
7
|
+
} from '@flighthq/bitmapfont';
|
|
8
|
+
import { createTextureAtlas } from '@flighthq/textureatlas';
|
|
9
|
+
import type { BitmapFontParseOptions, ImageResource, TextureAtlas } from '@flighthq/types';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { formatBitmapFontFnt, parseBitmapFontFnt } from './bitmapFontFnt';
|
|
13
|
+
|
|
14
|
+
const FNT_MULTIPAGE = [
|
|
15
|
+
'info face="Test" size=32 unicode=1',
|
|
16
|
+
'common lineHeight=32 base=26 scaleW=64 scaleH=64 pages=2 packed=0',
|
|
17
|
+
'page id=0 file="test_0.png"',
|
|
18
|
+
'page id=1 file="test_1.png"',
|
|
19
|
+
'chars count=2',
|
|
20
|
+
'char id=65 x=0 y=0 width=7 height=8 xoffset=1 yoffset=0 xadvance=9 page=0 chnl=15',
|
|
21
|
+
'char id=66 x=3 y=0 width=6 height=8 xoffset=0 yoffset=0 xadvance=8 page=1 chnl=15',
|
|
22
|
+
].join('\n');
|
|
23
|
+
|
|
24
|
+
const FNT_TEXT = [
|
|
25
|
+
'info face="Test" size=32 bold=0 italic=0 unicode=1 padding=0,0,0,0 spacing=1,1',
|
|
26
|
+
'common lineHeight=32 base=26 scaleW=64 scaleH=64 pages=1 packed=0',
|
|
27
|
+
'page id=0 file="test_0.png"',
|
|
28
|
+
'chars count=2',
|
|
29
|
+
'char id=65 x=0 y=0 width=7 height=8 xoffset=1 yoffset=5 xadvance=9 page=0 chnl=15',
|
|
30
|
+
'char id=86 x=8 y=0 width=6 height=8 xoffset=0 yoffset=0 xadvance=8 page=0 chnl=15',
|
|
31
|
+
'kernings count=1',
|
|
32
|
+
'kerning first=65 second=86 amount=-2',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
describe('formatBitmapFontFnt', () => {
|
|
36
|
+
it('round-trips a parsed font through parse → format → parse', () => {
|
|
37
|
+
const options = pageOptions();
|
|
38
|
+
const font = parseBitmapFontFnt(FNT_TEXT, options);
|
|
39
|
+
expect(font).not.toBeNull();
|
|
40
|
+
|
|
41
|
+
const reparsed = parseBitmapFontFnt(formatBitmapFontFnt(font!), options);
|
|
42
|
+
expect(reparsed).not.toBeNull();
|
|
43
|
+
|
|
44
|
+
expect(getBitmapFontGlyph(reparsed!, 65)).toEqual(getBitmapFontGlyph(font!, 65));
|
|
45
|
+
expect(getBitmapFontGlyph(reparsed!, 86)).toEqual(getBitmapFontGlyph(font!, 86));
|
|
46
|
+
expect(getBitmapFontKerning(reparsed!, 65, 86)).toBe(-2);
|
|
47
|
+
expect(getBitmapFontMetrics(reparsed!)).toEqual(getBitmapFontMetrics(font!));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('emits an empty page file reference (the atlas is a live resource, not a path)', () => {
|
|
51
|
+
const font = parseBitmapFontFnt(FNT_TEXT, pageOptions());
|
|
52
|
+
expect(formatBitmapFontFnt(font!)).toContain('page id=0 file=""');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('parseBitmapFontFnt', () => {
|
|
57
|
+
it('parses glyphs, kerning, and line metrics with the resolved atlas', () => {
|
|
58
|
+
const atlas = createTextureAtlas();
|
|
59
|
+
const font = parseBitmapFontFnt(FNT_TEXT, { resolvePage: () => atlas });
|
|
60
|
+
expect(font).not.toBeNull();
|
|
61
|
+
|
|
62
|
+
expect(getBitmapFontPage(font!, 0)).toBe(atlas);
|
|
63
|
+
// yoffset=5 with base=26 → bearingY = base - yoffset = 21 (baseline-relative, up-positive).
|
|
64
|
+
expect(getBitmapFontGlyph(font!, 65)).toEqual({
|
|
65
|
+
advance: 9,
|
|
66
|
+
bearingX: 1,
|
|
67
|
+
bearingY: 21,
|
|
68
|
+
height: 8,
|
|
69
|
+
page: 0,
|
|
70
|
+
width: 7,
|
|
71
|
+
x: 0,
|
|
72
|
+
y: 0,
|
|
73
|
+
});
|
|
74
|
+
// yoffset=0 → bearingY = base = 26 (glyph top at the line top, one full ascent above the baseline).
|
|
75
|
+
expect(getBitmapFontGlyph(font!, 86)).toEqual({
|
|
76
|
+
advance: 8,
|
|
77
|
+
bearingX: 0,
|
|
78
|
+
bearingY: 26,
|
|
79
|
+
height: 8,
|
|
80
|
+
page: 0,
|
|
81
|
+
width: 6,
|
|
82
|
+
x: 8,
|
|
83
|
+
y: 0,
|
|
84
|
+
});
|
|
85
|
+
expect(getBitmapFontKerning(font!, 65, 86)).toBe(-2);
|
|
86
|
+
expect(getBitmapFontKerning(font!, 86, 65)).toBe(0);
|
|
87
|
+
expect(getBitmapFontMetrics(font!)).toEqual({ ascent: 26, descent: 6, lineGap: 0 });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('passes the parsed page id and file to the resolver', () => {
|
|
91
|
+
const seen: Array<[number, string]> = [];
|
|
92
|
+
parseBitmapFontFnt(FNT_TEXT, {
|
|
93
|
+
resolvePage: (id, file) => {
|
|
94
|
+
seen.push([id, file]);
|
|
95
|
+
return createTextureAtlas();
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
expect(seen).toEqual([[0, 'test_0.png']]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('resolves every page of a multi-page font and routes each glyph to the right page image', () => {
|
|
102
|
+
const image0 = {} as ImageResource;
|
|
103
|
+
const image1 = {} as ImageResource;
|
|
104
|
+
const page0 = createTextureAtlas({ image: image0 });
|
|
105
|
+
const page1 = createTextureAtlas({ image: image1 });
|
|
106
|
+
const seen: Array<[number, string]> = [];
|
|
107
|
+
const font = parseBitmapFontFnt(FNT_MULTIPAGE, {
|
|
108
|
+
resolvePage: (id, file) => {
|
|
109
|
+
seen.push([id, file]);
|
|
110
|
+
return id === 0 ? page0 : page1;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
expect(font).not.toBeNull();
|
|
114
|
+
|
|
115
|
+
expect(seen).toEqual([
|
|
116
|
+
[0, 'test_0.png'],
|
|
117
|
+
[1, 'test_1.png'],
|
|
118
|
+
]);
|
|
119
|
+
expect(font!.pages).toEqual([page0, page1]);
|
|
120
|
+
expect(getBitmapFontPage(font!, 0)).toBe(page0);
|
|
121
|
+
expect(getBitmapFontPage(font!, 1)).toBe(page1);
|
|
122
|
+
expect(getBitmapFontGlyph(font!, 65)!.page).toBe(0);
|
|
123
|
+
expect(getBitmapFontGlyph(font!, 66)!.page).toBe(1);
|
|
124
|
+
|
|
125
|
+
const source = createGlyphSourceFromBitmapFont(font!);
|
|
126
|
+
expect(source.getGlyphAtlasImage(source.getGlyphEntry(65)!.page)).toBe(image0);
|
|
127
|
+
expect(source.getGlyphAtlasImage(source.getGlyphEntry(66)!.page)).toBe(image1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns null for malformed text without throwing', () => {
|
|
131
|
+
expect(parseBitmapFontFnt('this is not a bitmap font', pageOptions())).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns null when no resolvePage is supplied (the page is dropped)', () => {
|
|
135
|
+
expect(parseBitmapFontFnt(FNT_TEXT)).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns null when the resolver cannot resolve the page', () => {
|
|
139
|
+
expect(parseBitmapFontFnt(FNT_TEXT, { resolvePage: () => null })).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
function pageOptions(): BitmapFontParseOptions {
|
|
144
|
+
const atlas: TextureAtlas = createTextureAtlas();
|
|
145
|
+
return { resolvePage: () => atlas };
|
|
146
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBitmapFontGlyph,
|
|
3
|
+
getBitmapFontKerning,
|
|
4
|
+
getBitmapFontMetrics,
|
|
5
|
+
getBitmapFontPage,
|
|
6
|
+
} from '@flighthq/bitmapfont';
|
|
7
|
+
import { createTextureAtlas } from '@flighthq/textureatlas';
|
|
8
|
+
import type { BitmapFont } from '@flighthq/types';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { parseBitmapFontFnt } from './bitmapFontFnt';
|
|
12
|
+
import { parseBitmapFontJson } from './bitmapFontJson';
|
|
13
|
+
import { parseBitmapFontXml } from './bitmapFontXml';
|
|
14
|
+
|
|
15
|
+
const FNT_JSON = JSON.stringify({
|
|
16
|
+
chars: [
|
|
17
|
+
{ chnl: 15, height: 8, id: 65, page: 0, width: 7, x: 0, xadvance: 9, xoffset: 1, y: 0, yoffset: 5 },
|
|
18
|
+
{ chnl: 15, height: 8, id: 86, page: 0, width: 6, x: 8, xadvance: 8, xoffset: 0, y: 0, yoffset: 0 },
|
|
19
|
+
],
|
|
20
|
+
common: { base: 26, lineHeight: 32, pages: 1, scaleH: 64, scaleW: 64 },
|
|
21
|
+
info: { face: 'Test', size: 32 },
|
|
22
|
+
kernings: [{ amount: -2, first: 65, second: 86 }],
|
|
23
|
+
pages: ['test_0.png'],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const FNT_TEXT = [
|
|
27
|
+
'common lineHeight=32 base=26 scaleW=64 scaleH=64 pages=1',
|
|
28
|
+
'page id=0 file="test_0.png"',
|
|
29
|
+
'chars count=2',
|
|
30
|
+
'char id=65 x=0 y=0 width=7 height=8 xoffset=1 yoffset=5 xadvance=9 page=0 chnl=15',
|
|
31
|
+
'char id=86 x=8 y=0 width=6 height=8 xoffset=0 yoffset=0 xadvance=8 page=0 chnl=15',
|
|
32
|
+
'kernings count=1',
|
|
33
|
+
'kerning first=65 second=86 amount=-2',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
const FNT_XML = [
|
|
37
|
+
'<font>',
|
|
38
|
+
' <common lineHeight="32" base="26" scaleW="64" scaleH="64" pages="1"/>',
|
|
39
|
+
' <pages><page id="0" file="test_0.png"/></pages>',
|
|
40
|
+
' <chars count="2">',
|
|
41
|
+
' <char id="65" x="0" y="0" width="7" height="8" xoffset="1" yoffset="5" xadvance="9"/>',
|
|
42
|
+
' <char id="86" x="8" y="0" width="6" height="8" xoffset="0" yoffset="0" xadvance="8"/>',
|
|
43
|
+
' </chars>',
|
|
44
|
+
' <kernings count="1"><kerning first="65" second="86" amount="-2"/></kernings>',
|
|
45
|
+
'</font>',
|
|
46
|
+
].join('\n');
|
|
47
|
+
|
|
48
|
+
describe('parseBitmapFontJson', () => {
|
|
49
|
+
it('parses glyphs, kerning, and line metrics with the resolved atlas', () => {
|
|
50
|
+
const atlas = createTextureAtlas();
|
|
51
|
+
const font = parseBitmapFontJson(FNT_JSON, { resolvePage: () => atlas });
|
|
52
|
+
expect(font).not.toBeNull();
|
|
53
|
+
|
|
54
|
+
expect(getBitmapFontPage(font!, 0)).toBe(atlas);
|
|
55
|
+
// yoffset=5 with base=26 → bearingY = base - yoffset = 21 (baseline-relative, up-positive).
|
|
56
|
+
expect(getBitmapFontGlyph(font!, 65)).toEqual({
|
|
57
|
+
advance: 9,
|
|
58
|
+
bearingX: 1,
|
|
59
|
+
bearingY: 21,
|
|
60
|
+
height: 8,
|
|
61
|
+
page: 0,
|
|
62
|
+
width: 7,
|
|
63
|
+
x: 0,
|
|
64
|
+
y: 0,
|
|
65
|
+
});
|
|
66
|
+
expect(getBitmapFontGlyph(font!, 86)?.advance).toBe(8);
|
|
67
|
+
expect(getBitmapFontKerning(font!, 65, 86)).toBe(-2);
|
|
68
|
+
expect(getBitmapFontMetrics(font!)).toEqual({ ascent: 26, descent: 6, lineGap: 0 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('resolves both pages of a multi-page JSON export and routes each glyph to its page', () => {
|
|
72
|
+
const page0 = createTextureAtlas();
|
|
73
|
+
const page1 = createTextureAtlas();
|
|
74
|
+
const multiPage = JSON.stringify({
|
|
75
|
+
chars: [
|
|
76
|
+
{ height: 8, id: 65, page: 0, width: 7, x: 0, xadvance: 9, xoffset: 1, y: 0, yoffset: 0 },
|
|
77
|
+
{ height: 8, id: 66, page: 1, width: 6, x: 3, xadvance: 8, xoffset: 0, y: 0, yoffset: 0 },
|
|
78
|
+
],
|
|
79
|
+
common: { base: 26, lineHeight: 32 },
|
|
80
|
+
kernings: [],
|
|
81
|
+
pages: ['test_0.png', 'test_1.png'],
|
|
82
|
+
});
|
|
83
|
+
const font = parseBitmapFontJson(multiPage, { resolvePage: (id) => (id === 0 ? page0 : page1) });
|
|
84
|
+
expect(font).not.toBeNull();
|
|
85
|
+
|
|
86
|
+
expect(font!.pages).toEqual([page0, page1]);
|
|
87
|
+
expect(getBitmapFontPage(font!, 1)).toBe(page1);
|
|
88
|
+
expect(getBitmapFontGlyph(font!, 65)!.page).toBe(0);
|
|
89
|
+
expect(getBitmapFontGlyph(font!, 66)!.page).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('reads the sdf/msdf encoding from a distanceField block', () => {
|
|
93
|
+
const withField = JSON.parse(FNT_JSON) as Record<string, unknown>;
|
|
94
|
+
withField.distanceField = { distanceRange: 4, fieldType: 'msdf' };
|
|
95
|
+
const font = parseBitmapFontJson(JSON.stringify(withField), { resolvePage: () => createTextureAtlas() });
|
|
96
|
+
expect(font!.encoding).toBe('msdf');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('parses equivalently to the text and XML variants', () => {
|
|
100
|
+
const atlas = createTextureAtlas();
|
|
101
|
+
const resolvePage = () => atlas;
|
|
102
|
+
const fromJson = parseBitmapFontJson(FNT_JSON, { resolvePage })!;
|
|
103
|
+
const fromText = parseBitmapFontFnt(FNT_TEXT, { resolvePage })!;
|
|
104
|
+
const fromXml = parseBitmapFontXml(FNT_XML, { resolvePage })!;
|
|
105
|
+
|
|
106
|
+
for (const codepoint of [65, 86]) {
|
|
107
|
+
const glyph = getBitmapFontGlyph(fromJson, codepoint);
|
|
108
|
+
expect(getBitmapFontGlyph(fromText, codepoint)).toEqual(glyph);
|
|
109
|
+
expect(getBitmapFontGlyph(fromXml, codepoint)).toEqual(glyph);
|
|
110
|
+
}
|
|
111
|
+
expectSameKerningAndMetrics(fromText, fromJson);
|
|
112
|
+
expectSameKerningAndMetrics(fromXml, fromJson);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns null for malformed JSON without throwing', () => {
|
|
116
|
+
expect(parseBitmapFontJson('{ not json', { resolvePage: () => createTextureAtlas() })).toBeNull();
|
|
117
|
+
expect(parseBitmapFontJson('{}', { resolvePage: () => createTextureAtlas() })).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns null when no resolvePage is supplied', () => {
|
|
121
|
+
expect(parseBitmapFontJson(FNT_JSON)).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function expectSameKerningAndMetrics(actual: BitmapFont, expected: BitmapFont): void {
|
|
126
|
+
expect(getBitmapFontKerning(actual, 65, 86)).toBe(getBitmapFontKerning(expected, 65, 86));
|
|
127
|
+
expect(getBitmapFontMetrics(actual)).toEqual(getBitmapFontMetrics(expected));
|
|
128
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBitmapFontGlyph,
|
|
3
|
+
getBitmapFontKerning,
|
|
4
|
+
getBitmapFontMetrics,
|
|
5
|
+
getBitmapFontPage,
|
|
6
|
+
} from '@flighthq/bitmapfont';
|
|
7
|
+
import { createTextureAtlas } from '@flighthq/textureatlas';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { buildBitmapFontFromRecord } from './bitmapFontRecord';
|
|
11
|
+
import type { BitmapFontRecord } from './bitmapFontRecord';
|
|
12
|
+
|
|
13
|
+
describe('buildBitmapFontFromRecord', () => {
|
|
14
|
+
it('maps chars, kernings, and common metrics onto a BitmapFont', () => {
|
|
15
|
+
const atlas = createTextureAtlas();
|
|
16
|
+
const font = buildBitmapFontFromRecord(sampleRecord(), { resolvePage: () => atlas });
|
|
17
|
+
expect(font).not.toBeNull();
|
|
18
|
+
|
|
19
|
+
expect(getBitmapFontPage(font!, 0)).toBe(atlas);
|
|
20
|
+
// yoffset=5 with base=26 → bearingY = base - yoffset = 21 (baseline-relative, up-positive).
|
|
21
|
+
expect(getBitmapFontGlyph(font!, 65)).toEqual({
|
|
22
|
+
advance: 9,
|
|
23
|
+
bearingX: 1,
|
|
24
|
+
bearingY: 21,
|
|
25
|
+
height: 8,
|
|
26
|
+
page: 0,
|
|
27
|
+
width: 7,
|
|
28
|
+
x: 0,
|
|
29
|
+
y: 0,
|
|
30
|
+
});
|
|
31
|
+
expect(getBitmapFontKerning(font!, 65, 86)).toBe(-2);
|
|
32
|
+
// base=26, lineHeight=32 → ascent=26, descent=6, lineGap=0.
|
|
33
|
+
expect(getBitmapFontMetrics(font!)).toEqual({ ascent: 26, descent: 6, lineGap: 0 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('carries the record encoding onto the font', () => {
|
|
37
|
+
const font = buildBitmapFontFromRecord(
|
|
38
|
+
{ ...sampleRecord(), encoding: 'sdf' },
|
|
39
|
+
{ resolvePage: () => createTextureAtlas() },
|
|
40
|
+
);
|
|
41
|
+
expect(font!.encoding).toBe('sdf');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('resolves every declared page and carries each char page onto its glyph', () => {
|
|
45
|
+
const page0 = createTextureAtlas();
|
|
46
|
+
const page1 = createTextureAtlas();
|
|
47
|
+
const font = buildBitmapFontFromRecord(multiPageRecord(), {
|
|
48
|
+
resolvePage: (id) => (id === 0 ? page0 : page1),
|
|
49
|
+
});
|
|
50
|
+
expect(font).not.toBeNull();
|
|
51
|
+
|
|
52
|
+
expect(font!.pages).toEqual([page0, page1]);
|
|
53
|
+
expect(getBitmapFontPage(font!, 0)).toBe(page0);
|
|
54
|
+
expect(getBitmapFontPage(font!, 1)).toBe(page1);
|
|
55
|
+
expect(getBitmapFontGlyph(font!, 65)!.page).toBe(0);
|
|
56
|
+
expect(getBitmapFontGlyph(font!, 86)!.page).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('tolerates an unreferenced page that fails to resolve', () => {
|
|
60
|
+
// Two pages declared, but every glyph samples page 0; page 1 (unreferenced) fails to resolve.
|
|
61
|
+
const page0 = createTextureAtlas();
|
|
62
|
+
const font = buildBitmapFontFromRecord(
|
|
63
|
+
{
|
|
64
|
+
...sampleRecord(),
|
|
65
|
+
pages: [
|
|
66
|
+
{ file: 'a.png', id: 0 },
|
|
67
|
+
{ file: 'b.png', id: 1 },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{ resolvePage: (id) => (id === 0 ? page0 : null) },
|
|
71
|
+
);
|
|
72
|
+
expect(font).not.toBeNull();
|
|
73
|
+
expect(getBitmapFontPage(font!, 0)).toBe(page0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns null when a referenced page fails to resolve', () => {
|
|
77
|
+
// Glyph 86 samples page 1, which resolves null → the parse collapses.
|
|
78
|
+
expect(
|
|
79
|
+
buildBitmapFontFromRecord(multiPageRecord(), { resolvePage: (id) => (id === 0 ? createTextureAtlas() : null) }),
|
|
80
|
+
).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns null when resolvePage is absent, missing, or returns null for the page', () => {
|
|
84
|
+
expect(buildBitmapFontFromRecord(sampleRecord())).toBeNull();
|
|
85
|
+
expect(buildBitmapFontFromRecord(sampleRecord(), { resolvePage: () => null })).toBeNull();
|
|
86
|
+
expect(
|
|
87
|
+
buildBitmapFontFromRecord({ ...sampleRecord(), pages: [] }, { resolvePage: () => createTextureAtlas() }),
|
|
88
|
+
).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function multiPageRecord(): BitmapFontRecord {
|
|
93
|
+
return {
|
|
94
|
+
base: 26,
|
|
95
|
+
chars: [
|
|
96
|
+
{ height: 8, id: 65, page: 0, width: 7, x: 0, xadvance: 9, xoffset: 1, y: 0, yoffset: 0 },
|
|
97
|
+
{ height: 8, id: 86, page: 1, width: 6, x: 8, xadvance: 8, xoffset: 0, y: 0, yoffset: 0 },
|
|
98
|
+
],
|
|
99
|
+
encoding: 'raster',
|
|
100
|
+
kernings: [],
|
|
101
|
+
lineHeight: 32,
|
|
102
|
+
pages: [
|
|
103
|
+
{ file: 'test_0.png', id: 0 },
|
|
104
|
+
{ file: 'test_1.png', id: 1 },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sampleRecord(): BitmapFontRecord {
|
|
110
|
+
return {
|
|
111
|
+
base: 26,
|
|
112
|
+
chars: [
|
|
113
|
+
{ height: 8, id: 65, page: 0, width: 7, x: 0, xadvance: 9, xoffset: 1, y: 0, yoffset: 5 },
|
|
114
|
+
{ height: 8, id: 86, page: 0, width: 6, x: 8, xadvance: 8, xoffset: 0, y: 0, yoffset: 0 },
|
|
115
|
+
],
|
|
116
|
+
encoding: 'raster',
|
|
117
|
+
kernings: [{ amount: -2, first: 65, second: 86 }],
|
|
118
|
+
lineHeight: 32,
|
|
119
|
+
pages: [{ file: 'test_0.png', id: 0 }],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBitmapFontGlyph,
|
|
3
|
+
getBitmapFontKerning,
|
|
4
|
+
getBitmapFontMetrics,
|
|
5
|
+
getBitmapFontPage,
|
|
6
|
+
} from '@flighthq/bitmapfont';
|
|
7
|
+
import { createTextureAtlas } from '@flighthq/textureatlas';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { parseBitmapFontXml } from './bitmapFontXml';
|
|
11
|
+
|
|
12
|
+
const FNT_XML = [
|
|
13
|
+
'<?xml version="1.0"?>',
|
|
14
|
+
'<font>',
|
|
15
|
+
' <info face="Test" size="32" unicode="1"/>',
|
|
16
|
+
' <common lineHeight="32" base="26" scaleW="64" scaleH="64" pages="1" packed="0"/>',
|
|
17
|
+
' <pages><page id="0" file="test_0.png"/></pages>',
|
|
18
|
+
' <chars count="2">',
|
|
19
|
+
' <char id="65" x="0" y="0" width="7" height="8" xoffset="1" yoffset="5" xadvance="9" page="0" chnl="15"/>',
|
|
20
|
+
' <char id="86" x="8" y="0" width="6" height="8" xoffset="0" yoffset="0" xadvance="8" page="0" chnl="15"/>',
|
|
21
|
+
' </chars>',
|
|
22
|
+
' <kernings count="1">',
|
|
23
|
+
' <kerning first="65" second="86" amount="-2"/>',
|
|
24
|
+
' </kernings>',
|
|
25
|
+
'</font>',
|
|
26
|
+
].join('\n');
|
|
27
|
+
|
|
28
|
+
describe('parseBitmapFontXml', () => {
|
|
29
|
+
it('parses glyphs, kerning, and line metrics with the resolved atlas', () => {
|
|
30
|
+
const atlas = createTextureAtlas();
|
|
31
|
+
const font = parseBitmapFontXml(FNT_XML, { resolvePage: () => atlas });
|
|
32
|
+
expect(font).not.toBeNull();
|
|
33
|
+
|
|
34
|
+
expect(getBitmapFontPage(font!, 0)).toBe(atlas);
|
|
35
|
+
// yoffset=5 with base=26 → bearingY = base - yoffset = 21 (baseline-relative, up-positive).
|
|
36
|
+
expect(getBitmapFontGlyph(font!, 65)).toEqual({
|
|
37
|
+
advance: 9,
|
|
38
|
+
bearingX: 1,
|
|
39
|
+
bearingY: 21,
|
|
40
|
+
height: 8,
|
|
41
|
+
page: 0,
|
|
42
|
+
width: 7,
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
});
|
|
46
|
+
expect(getBitmapFontGlyph(font!, 86)?.advance).toBe(8);
|
|
47
|
+
expect(getBitmapFontKerning(font!, 65, 86)).toBe(-2);
|
|
48
|
+
expect(getBitmapFontMetrics(font!)).toEqual({ ascent: 26, descent: 6, lineGap: 0 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('passes the parsed page id and file to the resolver', () => {
|
|
52
|
+
const seen: Array<[number, string]> = [];
|
|
53
|
+
parseBitmapFontXml(FNT_XML, {
|
|
54
|
+
resolvePage: (id, file) => {
|
|
55
|
+
seen.push([id, file]);
|
|
56
|
+
return createTextureAtlas();
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
expect(seen).toEqual([[0, 'test_0.png']]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns null for malformed XML without throwing', () => {
|
|
63
|
+
expect(parseBitmapFontXml('<font><common/></font>', { resolvePage: () => createTextureAtlas() })).toBeNull();
|
|
64
|
+
expect(parseBitmapFontXml('not xml at all', { resolvePage: () => createTextureAtlas() })).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns null when no resolvePage is supplied', () => {
|
|
68
|
+
expect(parseBitmapFontXml(FNT_XML)).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|