mirror of
https://gitee.com/anod/open_agb_firm.git
synced 2025-05-06 22:04:10 +08:00
补档:字体文件生成工具
This commit is contained in:
parent
a82c282ea9
commit
30c254d930
310
tools/acf-builder/buildacf.js
Normal file
310
tools/acf-builder/buildacf.js
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
const {open} = require("fs/promises");
|
||||||
|
const {argv} = require("process");
|
||||||
|
|
||||||
|
// ACF文件格式:
|
||||||
|
// 0-3:ACFv ACF为固定字符串,v表示版本号,从1开始
|
||||||
|
// 4-7:font bounding 来自bdf文件
|
||||||
|
// 8-9: fragment items size 字体数据开始的位置
|
||||||
|
// 10-:fragment items 分段描述数据,一个分段的unicode是连续的
|
||||||
|
//
|
||||||
|
// 分段描述数据内容
|
||||||
|
// 0-1:start unicode 本分段第一个unicode
|
||||||
|
// 2-3:end unicode 本分段最后一个unicode
|
||||||
|
// 5: padding size for unicode 本分段每个unicode的字体数据占用的字节长度
|
||||||
|
// 6: bbxd count in this fragment 本分段总共用到的bbxd种类
|
||||||
|
// other: 4*(bbxd count) 本分段用到的所有bbxd的原始数据
|
||||||
|
//
|
||||||
|
// bbxd数据格式:
|
||||||
|
// 4个6字节的整数(-32 ~ 31),表示BBX对应的内容,加上一个6字节的整数表示dwidth的第一个数据
|
||||||
|
//
|
||||||
|
// 字体数据
|
||||||
|
// 同一个fragment里面的所有unicode长度都是padding size,如果实际数据少的会补齐0
|
||||||
|
// 如果fragment的bbxd count为1,则字体数据就是直接boundingbox的所有点阵数据
|
||||||
|
// 如果fragment的bbxd count大于1,则字体数据是1字节的bbxd索引,后接boundingbox的所有点阵数据
|
||||||
|
|
||||||
|
const helper = () => {
|
||||||
|
const setBitAt = (bitarr, pos) => {
|
||||||
|
let byteIndex = Math.floor( pos / 8 );
|
||||||
|
let bitOffset = pos % 8;
|
||||||
|
bitarr[ byteIndex ] |= 1 << bitOffset;
|
||||||
|
}
|
||||||
|
const testBit = (byte, pos) => byte & ( 1 << pos);
|
||||||
|
const putS6 = (arr, val, pos) => {
|
||||||
|
// sign bit
|
||||||
|
if( val < 0 ) {
|
||||||
|
setBitAt( arr, pos );
|
||||||
|
val = -val;
|
||||||
|
}
|
||||||
|
++pos;
|
||||||
|
|
||||||
|
// digits
|
||||||
|
for( let i=1<<4; i > 0; i>>=1, pos++ ){
|
||||||
|
if( val & i ) {
|
||||||
|
setBitAt(arr, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {setBitAt, testBit, putS6}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDescript = () => {
|
||||||
|
let numChars, boundingbox;
|
||||||
|
let glyphs = []
|
||||||
|
let charGlyph = {}
|
||||||
|
let fragments = []
|
||||||
|
|
||||||
|
const apply = async ( cmd, params ) => {
|
||||||
|
if( cmd == "chars" ) {
|
||||||
|
[numChars] = params.map( n => Number(n) );
|
||||||
|
}
|
||||||
|
else if( cmd == "fontboundingbox" ) {
|
||||||
|
boundingbox = params.map( n => Number(n) );
|
||||||
|
}
|
||||||
|
else if( cmd == "startchar" ){
|
||||||
|
charGlyph = {};
|
||||||
|
|
||||||
|
if( fragments.length == 0 ){
|
||||||
|
fragments.push({ padding: 0, bbxd: new Map() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if( cmd == "encoding" ){
|
||||||
|
charGlyph.code = Number(params[0]);
|
||||||
|
}
|
||||||
|
else if( cmd == "dwidth" ){
|
||||||
|
charGlyph.dwidth = params.map( n => Number(n) );
|
||||||
|
}
|
||||||
|
else if( cmd == "bbx" ){
|
||||||
|
charGlyph.bbx = params.map( n => Number(n) );
|
||||||
|
}
|
||||||
|
else if( cmd == "bitmap" ){
|
||||||
|
charGlyph.recoding = true;
|
||||||
|
charGlyph.data = [];
|
||||||
|
}
|
||||||
|
else if( cmd == "endchar" ){
|
||||||
|
const {recoding, code, ...value} = charGlyph;
|
||||||
|
const [w, h] = charGlyph.bbx;
|
||||||
|
|
||||||
|
let fragment = fragments.at(-1);
|
||||||
|
if( !("start" in fragment) ){
|
||||||
|
fragment.start = code;
|
||||||
|
}
|
||||||
|
else if( glyphs.at(-1).code + 1 != code ){// break
|
||||||
|
fragment.end = glyphs.at(-1).code;
|
||||||
|
|
||||||
|
// new fragment
|
||||||
|
fragment = { start: code, padding: 0, bbxd: new Map() };
|
||||||
|
fragments.push( fragment );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( w*h > fragment.padding ){
|
||||||
|
fragment.padding = w*h;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = JSON.stringify( [...value.bbx, value.dwidth[0]] )
|
||||||
|
if( !fragment.bbxd.has(key) ){
|
||||||
|
fragment.bbxd.set( key, fragment.bbxd.size );
|
||||||
|
}
|
||||||
|
|
||||||
|
charGlyph = {};
|
||||||
|
glyphs.push( {...value, code, fragment} );
|
||||||
|
}
|
||||||
|
else if( cmd == "endfont" ){
|
||||||
|
const fragment = fragments.at(-1);
|
||||||
|
fragment.end = glyphs.at(-1).code;
|
||||||
|
}
|
||||||
|
else if( charGlyph.recoding ){
|
||||||
|
charGlyph.data.push( parseInt(cmd, 16) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const save = async file => {
|
||||||
|
const { setBitAt, testBit, putS6 } = helper();
|
||||||
|
const packHead = () => {
|
||||||
|
const buffer = new ArrayBuffer(6);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint16(0, numChars, true);
|
||||||
|
boundingbox.forEach( (c, i) => view.setInt8(2+i, c) )
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
const packHeadV2 = fragSize => {
|
||||||
|
const buffer = new ArrayBuffer(10);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint8(0, "A".charCodeAt())
|
||||||
|
view.setUint8(1, "C".charCodeAt())
|
||||||
|
view.setUint8(2, "F".charCodeAt())
|
||||||
|
view.setUint8(3, 2)//v2
|
||||||
|
boundingbox.forEach( (c, i) => view.setInt8(4+i, c) )
|
||||||
|
view.setUint16(8, fragSize, true)
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
const packGlyphs = () => {
|
||||||
|
return glyphs.map( glyph => [packChar(glyph), packPixel(glyph), glyph] )
|
||||||
|
}
|
||||||
|
const packChar = glyph => {
|
||||||
|
const buffer = new Uint8Array(4);
|
||||||
|
const bitwid = 6;
|
||||||
|
glyph.bbx.forEach( (c,i) => putS6(buffer, c, bitwid*i) );
|
||||||
|
putS6( buffer, glyph.dwidth[0], bitwid*4 );
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
const packPixel = glyph => {
|
||||||
|
const [w, h] = glyph.bbx;
|
||||||
|
const {padding} = glyph.fragment;
|
||||||
|
const nbyte = Math.ceil( padding / 8 );
|
||||||
|
const buffer = new Uint8Array( nbyte );
|
||||||
|
const base = 8 * ( (7+w) >> 3 ) - 1;
|
||||||
|
for( let j = 0; j < h; ++j ){
|
||||||
|
let linepixel = glyph.data[j];
|
||||||
|
for( let i = 0; i < w; ++i ){
|
||||||
|
if( testBit(linepixel, base-i) ){
|
||||||
|
setBitAt( buffer, j*w+i )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
const packIndex = fonts => {
|
||||||
|
const buffer = new ArrayBuffer( numChars * 5 );
|
||||||
|
const view = new DataView( buffer );
|
||||||
|
let offset = 0;
|
||||||
|
glyphs.forEach( (c, i) => {
|
||||||
|
view.setUint16( i*5, c.code, true );
|
||||||
|
view.setUint8( i*5 + 2, offset >> 16 );
|
||||||
|
view.setUint8( i*5 + 3, offset & 0xFF );
|
||||||
|
view.setUint8( i*5 + 4, (offset & 0xFFFF) >> 8 );
|
||||||
|
offset += 4 + fonts[i][1].length;
|
||||||
|
})
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
const packFragments = () => {
|
||||||
|
const bitwid = 6;
|
||||||
|
const serials = fragments.map( frag => {
|
||||||
|
const buffer = new ArrayBuffer( 6+4*frag.bbxd.size );
|
||||||
|
const view = new DataView( buffer );
|
||||||
|
const nbyte = Math.ceil( frag.padding / 8 );
|
||||||
|
view.setUint16( 0, frag.start, true );
|
||||||
|
view.setUint16( 2, frag.end, true );
|
||||||
|
view.setUint8( 4, frag.bbxd.size > 1 ? 1+nbyte : nbyte );
|
||||||
|
view.setUint8( 5, frag.bbxd.size );
|
||||||
|
|
||||||
|
frag.bbxd.forEach( (idx, key) => {
|
||||||
|
const arr = new Uint8Array(buffer, 6+idx*4, 4);
|
||||||
|
const embeded = JSON.parse( key );
|
||||||
|
embeded.forEach( (c,i)=>putS6(arr, c, bitwid*i) )
|
||||||
|
} )
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
})
|
||||||
|
return serials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* old code: pack for acfv1
|
||||||
|
const head = packHead();
|
||||||
|
const glyph = packGlyphs();
|
||||||
|
const index = packIndex( glyph );
|
||||||
|
|
||||||
|
let out = await open(file, "w");
|
||||||
|
await out.write(head);
|
||||||
|
await out.write(index);
|
||||||
|
let jobs = Promise.resolve();
|
||||||
|
glyph.forEach( ([char, pixel]) => {
|
||||||
|
const combine = Uint8Array.from([...char, ...pixel])
|
||||||
|
jobs = jobs.then( () => out.write(combine) )
|
||||||
|
} )
|
||||||
|
await jobs;
|
||||||
|
await out.close();
|
||||||
|
*/
|
||||||
|
|
||||||
|
const index = packFragments();
|
||||||
|
const head = packHeadV2( index.reduce( (r,b)=>r+b.length, 0 ) );
|
||||||
|
const glyph = packGlyphs();
|
||||||
|
|
||||||
|
let out = await open(file, "w");
|
||||||
|
await out.write( head );
|
||||||
|
let jobs = Promise.resolve();
|
||||||
|
index.forEach( buffer => {
|
||||||
|
jobs = jobs.then( () => out.write(buffer) )
|
||||||
|
} );
|
||||||
|
await jobs;
|
||||||
|
|
||||||
|
jobs = Promise.resolve();
|
||||||
|
glyph.forEach( ([char, pixel, glyph]) => {
|
||||||
|
const {fragment} = glyph;
|
||||||
|
if( fragment.bbxd.size == 1 ) jobs = jobs.then( () => out.write(pixel) )
|
||||||
|
else {
|
||||||
|
const key = JSON.stringify( [...glyph.bbx, glyph.dwidth[0]] )
|
||||||
|
const combine = Uint8Array.from( [fragment.bbxd.get(key), ...pixel] )
|
||||||
|
jobs = jobs.then( () => out.write(combine) )
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
await jobs;
|
||||||
|
await out.close();
|
||||||
|
}
|
||||||
|
return { apply, save }
|
||||||
|
}
|
||||||
|
|
||||||
|
const createReader = file => {
|
||||||
|
const newline = "\n".charCodeAt();
|
||||||
|
|
||||||
|
let buffer = new Uint8Array(400);
|
||||||
|
let cursor = buffer.length;
|
||||||
|
let line = ""
|
||||||
|
const readline = async () => {
|
||||||
|
if( cursor == buffer.length ){
|
||||||
|
cursor = 0;
|
||||||
|
await file.read( buffer, cursor, buffer.length )
|
||||||
|
}
|
||||||
|
|
||||||
|
let chars = []
|
||||||
|
let fullline = null;
|
||||||
|
while( cursor < buffer.length ){
|
||||||
|
const char = buffer[cursor++];
|
||||||
|
if( char == newline ) {
|
||||||
|
fullline = `${line}${String.fromCharCode(...chars)}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
chars.push( char )
|
||||||
|
}
|
||||||
|
|
||||||
|
if( fullline ){
|
||||||
|
line = ""; // reset line
|
||||||
|
return fullline.length > 0 ? fullline.split(" ").map( (word,i) => i==0 ? word.toLowerCase():word ) : [null];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
line = `${line}${String.fromCharCode(...chars)}`;
|
||||||
|
return await readline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readline;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convert = async fname => {
|
||||||
|
let bdf = await open( fname, "r" );
|
||||||
|
|
||||||
|
const readline = createReader( bdf );
|
||||||
|
const bdfdesc = createDescript();
|
||||||
|
|
||||||
|
while( true ){
|
||||||
|
let [cmd, ...params] = await readline();
|
||||||
|
|
||||||
|
await bdfdesc.apply( cmd, params )
|
||||||
|
|
||||||
|
if( cmd == "endfont" ) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await bdf.close();
|
||||||
|
await bdfdesc.save( fname.replace(".bdf", ".acf") );
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = async params => {
|
||||||
|
if( params.length < 1 ){
|
||||||
|
console.log("Usage: buildact file [file2] [...]")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat("zh-CN", {dateStyle: "short", timeStyle: "medium", timeZone: "Asia/Shanghai"})
|
||||||
|
globalThis.debug = log => console.log( `[DEBUG][${formatter.format(new Date())}]${log}` )
|
||||||
|
await Promise.all( params.map( file => convert(file) ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
start( argv.slice(2) );
|
Loading…
x
Reference in New Issue
Block a user