lzhとはlz+huffman符号による圧縮法の一般的な呼び方です(という事にしておきます)。295 bytesで展開できるなら、それなりに小さい方でしょう。その実体は以下の通り。
//base85形式文字列からbit単位の読み込み
b=c=>{if(!n){i+=5;for(a=0;n<5;a=a*85+(c<91?c:57))c=s.charCodeAt(i-++n)-35;n=32}return a>>--n&1},
//複数bit読み込み
v=c=>(--c&&v(c)*2)+b(),
//huffman木読み込み
h=c=>b()?[h(),h()]:v(8),
//huffman符号復号
d=c=>c[0]?d(c[b()]):c,
s="",//圧縮文
S="";//展開先文字列
//lz系列展開. Nは元の大きさ
for(h=h(e=h(i=a=n=0));S.length<N;S+=c<128?String.fromCharCode(c):S.substr(S.length-d(e)*256-v(8),c-128))c=d(h)
圧縮programは以下の通り。最長一致127、窓幅65535となっています。ASCII文字(7bit)のみに対応しています。
文字と一致長を共通のhuffman木、距離の上位8bitは別のhuffman木で符号化。距離の下位8bitはそのまま書き込み。
a2lzh.js
// lz+huffman compressor(zopfli-style iteration, binary-tree match finder)
const huff=f=>{
const s=[],q=[],qw=[];let i=0,j=0;
f.forEach((w,x)=>w&&s.push(x));s.length||s.push(0);
s.sort((a,b)=>f[a]-f[b]);
const pop=()=>i<s.length&&(j==q.length||f[s[i]]<=qw[j])?[s[i],f[s[i++]]]:[q[j],qw[j++]];
for(let m=s.length;--m;){
const[a,x]=pop(),[b,y]=pop();
q.push(a?[a,b]:[b,0]); // leaf 0 must not be the left child
qw.push(x+y)
}
return s.length>1?q[q.length-1]:s[0]
},
code=(t,v=0,k=0,o=[[],[]])=>(Array.isArray(t)?(code(t[0],v*2,k+1,o),code(t[1],v*2+1,k+1,o)):(o[0][t]=v,o[1][t]=k),o),
emit=(t,put)=>Array.isArray(t)?(put(1,1),emit(t[0],put),emit(t[1],put)):(put(0,1),put(t,8)),
cost=(l,f)=>{const x=l.reduce((m,k)=>Math.max(m,k),0)+10;return f.map((w,s)=>w?l[s]:x)};
// binary tree match finder: M[p]=[len0,dist0,len1,dist1,...] (len, dist ascending; len<=dist)
function find(b,n,nice,depth){
const head=new Int32Array(1<<14).fill(-1),son=new Int32Array(2*n).fill(-1),M=[];
for(let p=0;p<n;p++){
const m=M[p]=[];
if(p+2>n)continue;
let h=b[p]<<7|b[p+1],c=head[h],lo=2*p,hi=lo+1,ll=2,lh=2,best=1,k=depth;
for(head[h]=p;;){
const d=p-c;
if(c<0||!k--||d>65535){son[lo]=son[hi]=-1;break}
let l=ll<lh?ll:lh;
while(l<nice&&b[c+l]===b[p+l])l++;
const u=Math.min(l,d,n-p);
u>best&&m.push(best=u,d);
if(l===nice&&d>=nice){son[lo]=son[2*c];son[hi]=son[2*c+1];break}
if(b[c+l]<b[p+l]||l===nice)son[lo]=c,c=son[lo=2*c+1],ll=l;
else son[hi]=c,c=son[hi=2*c],lh=l
}
}return M
}
// optimal parse by DP under the current cost model (bits per symbol)
function parse(M,b,n,cH,cA){
const from=new Int32Array(n+1),dist=new Int32Array(n+1);
for(let cost=new Float64Array(n+1).fill(1/0),i=cost[0]=0;i<n;i++){
const c=cost[i],m=M[i];
if(c+cH[b[i]]<cost[i+1])cost[i+1]=c+cH[b[i]],from[i+1]=i,dist[i+1]=0;
for(let j=0,k=m.length,L=2;j<k;)
for(let l=m[j++],d=m[j++],e=c+cA[d>>8]+8,t;L<=l;L++){
t=e+cH[128+L];
if(t<cost[i+L])cost[i+L]=t,from[i+L]=i,dist[i+L]=d
}
}
const tok=[];
for(let i=n;i;i=from[i])tok.push(dist[i]?[128+i-from[i],dist[i]]:[b[i-1],0]);
return tok.reverse()
}
// huffman-code the tokens; returns [bits, next cost model for H, for A]
function enc(tok){
const fH=new Uint32Array(256),fA=new Uint32Array(256),w=[];let m,n=32,p=0;
for(let[s,d]of tok)fH[s]++,d&&fA[d>>8]++;
const tH=huff(fH),tA=huff(fA),[vH,lH]=code(tH),[vA,lA]=code(tA);
const put=(v,k)=>{ // v<2**k, k<33, MSB first
if(n>k)return m|=v<<(n-=k);
n=32-(k-=n);
w[p++]=m|=v>>>k;
m=n<32?v<<n:0
};
emit(tA,put);emit(tH,put);
for(let[s,d]of tok){put(vH[s],lH[s]);if(d){put(vA[d>>8],lA[d>>8]);put(d&255,8)}}
put(0,31);
return[w,p*8+32-n,cost(lH,fH),cost(lA,fA)];
}
function compress(src,{iter=16,nice=127,depth=64}={}){
nice=Math.max(2,Math.min(nice,127));
const n=src.length,b=new Uint8Array(n+nice+1),f=Array(128).fill(0);
for(let i=0;i<n;i++){if((b[i]=src.charCodeAt(i))>127)throw Error("ASCII only");f[b[i]]++}
const M=find(b,n,nice,depth);
let best=enc(Array.from(b.subarray(0,n),c=>[c,0])); // baseline: literals only
for(const pen of[0,9]){ // two initial models: neutral / matches penalised (avoids bad local optima)
let cH=Array.from({length:256},(_,s)=>s>127?7+pen:f[s]?Math.log2(n/f[s]):12),cA=Array(256).fill(4+pen),last;
for(let k=iter,r;k--;){
r=enc(parse(M,b,n,cH,cA));
if(r[1]<best[1])best=r;
if(r[1]===last)break;
[,last,cH,cA]=r
}
}
let s="";src="#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[~]^_`abcdefghijklmnopqrstuvw";
for(const x of best[0])for(let a=x>>>0,j=5;j--;a=a/85|0)s+=src[a%85]; // 5 digits/word, least significant first
return s
}
動作用html
<!doctype html><html><head><meta charset=sjis><title>a2lzh</title><style>
*{color:snow;font-family:FixedSys,monospace;font-size:1em;box-sizing:border-box;margin:0}
body{background:#212;height:100vh;overflow:hidden}
a{color:#aaf;text-decoration:none}
a:hover,label:hover,input:hover{background:#a6a}
input,label,select{background:#848;border:1px solid #c6c;border-radius:4px;cursor:pointer;display:inline-block;padding:3px 6px}
.tabs{padding:0;width:100%;height:100%;float:none;list-style:none;position:relative}
.tabs>li{float:left;margin:0 2px 0 0}
.tabs>li>[type=radio],[type=file]{display:none}
.tabs>li>:checked+label{top:0;background:#424;border-bottom:0}
.tabs>li>:checked~.tabc{display:block}
.tabs>li>label{padding:9px;position:relative;top:4px;transition:.2s ease-in-out}
.tabs .tabc{background:radial-gradient(circle at center center,#a6a,#212);display:none;overflow:auto;height:calc(100% - 2em);width:100%;position:absolute;left:0;z-index:2}
body,#t2t{display:flex;flex-direction:column}
#it,#ot{background:transparent;border:1px solid #222;flex:1;overflow:auto;resize:none}
#t2t p,#log{flex:0 0 1em;width:100%}
.tabc>p{text-align:center}
ol{border:2px dashed #caf;overflow:auto;background:#3236;position:absolute;top:1.5em;bottom:0;left:0;right:0}
ol:empty:before{content:"Drop files here";display:block;text-align:center;position:relative;top:45%}
li{margin-bottom:3px}
.dover{background:#535}
.dpass{background:#323}
/* modal main */
.modalson{
background:#323a;
overflow:hidden;
position:fixed;
transition:all .2s;
z-index:9
}
.modalson>label+*{overflow:auto}
.modal>input{display:none}
.modalson>label[for]{
display:block;width:100%;
text-align:center
}
/* modal size */
.m-up{top:100%;left:0;right:0;bottom:1.25em}
/* modal switch */
.modal input:checked~.m-up{top:55%;bottom:1.3em}
</style></head><body>
<ul class=tabs>
<li>
<input type=radio name=tabs id=tab1 checked>
<label for=tab1>Text</label>
<form class=tabc id=t2t>
<textarea placeholder=input id=it></textarea>
<p><input type=button value="vary↓"id=en>
<input type=button value="undo↑"id=de>
<label for=opts>≡</label>
<input type=reset value=reset></p>
<textarea placeholder=output id=ot></textarea>
</form>
</li>
<li>
<input type=radio name=tabs id=tab2>
<label for=tab2>File</label>
<div class=tabc><p>
<label id=kill>kill</label>
<label><input type=file multiple id=load>LOAD</label>
<label id=save>Save</label>
<label for=opts>≡</label>
<ol id=flist></ol>
</div>
</li>
<li>
<input type=radio name=tabs id=tab3>
<label for=tab3>detail</label>
<div class=tabc>
lz+huffman系圧縮program(最長一致127、窓幅65535)。ASCII文字列(7bit)のみ対応
<ul>
<li>Text<ul>
<li>vary<p>入力文字列を圧縮
<li>undo<p>圧縮結果を展開
</ul>
<li>File<ul>
<li>kill<p>処理結果を全削除
<li>LOAD<p>複数fileを選択して読み込む。<b>js file</b>のみ対応(evalに渡される)
<li>Save<p>処理結果を全保存
<li>Drop files here<p>複数file投下しやがってください。まとめて処理します
</ul>
<li>iter<p>最適化回数
<li>nice length<p>最長一致を打ち切る長さ。大きい程高圧縮で低速
<li>search depth<p>最長一致の深さ。大きい程精度は良いが、速度が犠牲になる
<li>report<p>圧縮率等を表示
</ul>
</div>
</li>
</ul>
<input id=log value=report>
<div class=modal>
<input type=checkbox id=opts>
<div class="modalson m-up"><label for=opts>options</label>
<form id=oform>
iter<input id=iter value=9 size=8> nice length<input id=nice value=127 size=3> search depth<input id=dp value=48 size=4>
</form>
</div>
</div><script src="a2lzh.js"></script><script>{
const exe=['b=c=>{if(!n){i+=5;for(a=0;n<5;a=a*85+(c<91?c:57))c=s.charCodeAt(i-++n)-35;n=32}return a>>--n&1},v=c=>(--c&&v(c)*2)+b(),h=c=>b()?[h(),h()]:v(8),d=c=>c[0]?d(c[b()]):c,s="',,
'",S="";for(h=h(e=h(i=a=n=0));S.length<',,
';S+=c<128?String.fromCharCode(c):S.substr(S.length-d(e)*256-v(8),c-128))c=d(h)'],
g=a=>document.getElementById(a),
log=g("log"),ol=g("flist"),lf=g("load"),en=g("en"),de=g("de"),
u8s=(s,b=s.length)=>{
for(s of s)if((s=s.codePointAt())>127)if(b++,s>2047)b++,s>65535&&b++;
return b
};
ol.ondragover=function(e){
this.className="dover";e.preventDefault();
e.dataTransfer.dropEffect='copy'
};
ol.ondragleave=function(){this.className="dpass"};
ol.ondrop=lf.oninput=async function(e){
e.preventDefault(e.stopPropagation());
ol.className="dpass";
var d=0,f=(e.dataTransfer||this).files,r,s=0,t=0,st=new Date;
for(e of f)t+=e.size;
//write packed data
let pf=r=>e.text().then(b=>{
let c=exe.slice(),z=c[3]=b.length,n=e.name,li=document.createElement("li");
try{
c[1]=compress(b,{iter:iter.value>>>0,nice:nice.value>>>0,depth:dp.value>>>0});
s+=z;c='eval(((a,c,e,i,n)=>{let '+c.join``+';return S})())';
li.innerHTML=`<a href=${URL.createObjectURL(new Blob([c]))} download='${n}.hlz.js'>${n}.hlz.js</a> <input type=button value="${z+" -> "+(e=c.length)}">`
}catch(b){li.innerHTML=n+": "+b;t-=z;e=0}
ol.appendChild(li);
d+=e;r(log.value=log.style.width=(s/t*1e4|0)/100+"%")
});
for(e of f)await new Promise(pf);
log.value=`${t+" -> "+d}(${(d/t*1e4|0)/100}%), ${new Date-st}ms`
};
// remove files
g("kill").onclick=function(){
for(var e=ol.getElementsByTagName("a"),b=0,c;c=e[b++];)c.download&&URL.revokeObjectURL(c.href);
if(b>1)ol.innerHTML=""
};
// save files
g("save").onclick=function(){
for(var b=ol.getElementsByTagName("a"),c=0,d;d=b[c++];)d.click();
if(c<2)b=this,b.innerText="miss",setTimeout(function(){b.innerText="Save"},500)
};
en.onclick=a=>{
let b=it.value,c=exe.slice(),z=c[3]=b.length,t=new Date;
c[1]=compress(b,{iter:iter.value>>>0,nice:nice.value>>>0,depth:dp.value>>>0});
log.style.width="100%";log.value=z+" -> "+(ot.value=c.join``).length+" in "+(new Date-t)+"ms"
};
de.onclick=a=>{
let b=ot.value,z=u8s(b),t=new Date;
it.value=a=eval("((a,c,e,i,n)=>{let "+ot.value+";return S})()");
log.style.width="100%";log.value=z+" -> "+u8s(a)+" in "+(new Date-t)+"ms"
};
log.onclick=(a,b=ot.value)=>{
if(tab1.checked&&b)
a=document.createElement("a"),
a.download="a.htm",
URL.revokeObjectURL(a.href=URL.createObjectURL(new Blob([s2b(b)])),a.click());
};
ol.onclick=function(a){
(a=a.target).type=="button"&&confirm`remove?`&&(a=a.parentNode).remove(URL.revokeObjectURL(a.firstChild.href))
}}</script>
codepen
See the Pen Tiny lzh codec by xezz (@xezz) on CodePen.
textareaに出力される文字列は圧縮文+展開処理となっています。展開された文字列は変数Sに格納されているので、eval(S)やdocument.write(S)などといった処理を補完して使って下さい。file処理の場合はjs fileのみにして下さい。展開文字列はeval()に放り込まれます