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