0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

lzhの295 bytes展開program

0
Last updated at Posted at 2026-11-08

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&#8595"id=en>
			<input type=button value="undo&#8593"id=de>
			<label for=opts>&#8801</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>&#8801</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()に放り込まれます

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?