anod e484eb6202 !1 把dev_cheat的部分改动(基本上是acf)合并回去
* 修改好bug,改过的acf接口
* Merge branch 'dev_cheat' of https://gitee.com/anod/open_agb_firm into dev_cheat
* acf修改接口
* Merge branch 'dev_cheat' of https://gitee.com/anod/open_agb_firm into dev_cheat
* 修正:上个版本会访问非法内存导致段错误
* 补档:字体文件生成工具
2022-09-25 12:45:21 +00:00

310 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) );