From 30c254d930114482116154233853a0bdf0bc8468 Mon Sep 17 00:00:00 2001 From: anod <182859762@qq.com> Date: Fri, 12 Aug 2022 13:53:14 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E6=A1=A3=EF=BC=9A=E5=AD=97=E4=BD=93?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=94=9F=E6=88=90=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/acf-builder/buildacf.js | 310 ++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 tools/acf-builder/buildacf.js diff --git a/tools/acf-builder/buildacf.js b/tools/acf-builder/buildacf.js new file mode 100644 index 0000000..27bb602 --- /dev/null +++ b/tools/acf-builder/buildacf.js @@ -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) ); \ No newline at end of file