研究Google chart API的时候发现它提供的数据压缩算法不错,可以把纯数字的数组压缩成很短的一个字符串,分享如下:
var simpleEncoding = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; function simpleEncode(values,maxValue) { var chartData = ['s:']; for (var i = 0; i < values.length; i++) { var currentValue = values[i]; if (!isNaN(currentValue) && currentValue >= 0) { chartData.push(simpleEncoding.charAt(Math.round((simpleEncoding.length-1) * currentValue / maxValue))); } else { chartData.push('_'); } } return chartData.join(''); }
测试例子:
var valueArray = new Array(20,100,40,50,60,81,74); var maxValue = 100; document.write(simpleEncode(valueArray,maxValue)); |
二: 图像压缩
图像压缩是基于像素的,base64以后其实是文本,不需要找针对像素做的降低图片效果的失真压缩算法,只需要考虑文本本身的压缩即可。
文本的压缩基本上都是基于字符的重复频率来做的,明显base64只能出现64个字符,二进制的图片文件变成base64时增大1/3, 文件越大,base64重复越高。基于文本的压缩效率越高。
可以自行搜索 LZ77或LZW压缩算法的js实现,网上多的很。
LZW压缩算法js版本: http://hi.baidu.com/kooboy/item/12b5b489608928c698255fb5
// LZW-compress a stringfunctionlzw_encode(s){ vardict={}; vardata=(s+"").split(""); varout=[]; varcurrChar; varphrase=data[0]; varcode=256; for(vari=1;i<data.length;i++){ currChar=data[i]; if(dict[phrase+currChar]!=null){ phrase+=currChar; } else{ out.push(phrase.length>1?dict[phrase]:phrase.charCodeAt(0)); dict[phrase+currChar]=code; code++; phrase=currChar; } } out.push(phrase.length>1?dict[phrase]:phrase.charCodeAt(0)); for(vari=0;i<out.length;i++){ out[i]=String.fromCharCode(out[i]); } returnout.join("");}
// Decompress an LZW-encoded stringfunctionlzw_decode(s){ vardict={}; vardata=(s+"").split(""); varcurrChar=data[0]; varoldPhrase=currChar; varout=[currChar]; varcode=256; varphrase; for(vari=1;i<data.length;i++){ varcurrCode=data[i].charCodeAt(0); if(currCode<256){ phrase=data[i]; } else{ phrase=dict[currCode]?dict[currCode]:(oldPhrase+currChar); } out.push(phrase); currChar=phrase.charAt(0); dict[code]=oldPhrase+currChar; code++; oldPhrase=phrase; } returnout.join("");}
很不幸的是,在压缩的时候,红色粗体部分是有瑕疵的,比如你压缩这个字符串在ff浏览器下:
watchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatchwatch
因为标准浏览器下对象都有一个watch方法,所以会出问题,因此我改写了这个过程:
xcode=function(s,f){
if(!s)return '';
var dict = {},
out = [],
prefix = s.charAt(0),
curChar=prefix,
oldPrefix=curChar,
idx= 256,
i,c,d,
g=function(){
out.push(prefix.length > 1 ? String.fromCharCode(dict[prefix]) : prefix);
};
if(f){
out.push(prefix);
}
for (i=1,c,d; i<s.length; i++) {
c=s.charAt(i);
if(f){
d=s.charCodeAt(i);
prefix=d<256?c:dict[d]||(prefix+curChar);
out.push(prefix);
curChar=prefix.charAt(0);
dict[idx++]=oldPrefix+curChar;
oldPrefix=prefix;
}else{
if (dict.hasOwnProperty(prefix + c)) {
prefix += c;
}
else {
g();
dict[prefix + c] = idx++;
prefix=c;
}
}
}
if(!f)g();
return out.join('');
}
压缩时:xcode('watchwatch....');
解压时:xcode('compressed string',true);//第二个参数传true表示解压,反之为压缩
JS操作二进制很麻烦,而且一直没有一个好的无损压缩工具来实现纯文本的压缩。
所以钻研了一段时间的gzip,后来发现还是仅用 LZ77 比较容易实现,gzip中的 haffman 压缩部分对于JS来说太难搞了。
代码如下,注释的非常完整,所以就不多说了,有兴趣的可以仔细研究下:
<html> <head> <title>LZ77</title> <style> * { font-size:12px; } body { overflow:auto; background-color:buttonface; } textarea { width:100%; height:240px; overflow:auto; } #btn1 { width:100px; } </style> <script> window.onload = init; function $(s){ return document.getElementById(s); } function init() { $("txtS").focus(); $("btn1").onclick = run; $("txtS").onkeydown = function () { if (event.keyCode == 13 && event.ctrlKey) { run(); } } } function run() { var str = $("txtS").value; $("txtS").value = ""; var lzc = new Lz77CompressDefer(str); var t = new Date(); lzc.start(function (result) { $("txtR").value = Lz77SelfExtract(result); var tc = new Date() - t; $("txtS").value = eval($("txtR").value.substring(4)); var td = new Date() - t - tc; alert("压缩完毕\r\n压缩比:"+($("txtR").value.length/str.length*100).toFixed(2)+"%\r\n压缩用时:"+tc+"ms\r\n解压用时:"+td+"ms\r\n校验:"+(str==$("txtS").value?"OK":"failed")); }); function showProgress(){ var p = lzc.status(); if (p < 1) { $("txtS").value = "压缩中 ... " + (p*100).toFixed(2) + "%"; setTimeout(showProgress, 300); } } showProgress(); /* $("txtR").value = Lz77Compress(str); var tc = new Date() - t; $("txtS").value = Lz77Decompress($("txtR").value); var td = new Date() - t - tc; alert($("txtR").value.length/$("txtS").value.length+":"+tc+":"+td+":"+(str==$("txtS").value)); */ } /* * 以 LZ77 原理实现的JS文本压缩算法 * Author: Hutia * */ /* LZ77基本原理: 1、从当前压缩位置开始,考察未编码的数据,并试图在滑动窗口中找出最长的匹配字符串,如果找到,则进行步骤 2,否则进行步骤 3。 2、输出三元符号组 ( off, len, c )。其中 off 为窗口中匹配字符串相对窗口边界的偏移,len 为可匹配的长度,c 为下一个字符。然后将窗口向后滑动 len + 1 个字符,继续步骤 1。 3、输出三元符号组 ( 0, 0, c )。其中 c 为下一个字符。然后将窗口向后滑动 len + 1 个字符,继续步骤 1。 变种: 1. 将匹配串和不能匹配的单个字符分别编码、分别输出,输出匹配串时不同时输出后续字符。 本算法变种: 1. 采用出现概率很低的前导字符P来区分匹配串输出和非匹配串。对于匹配串,输出 ( P, off, len ),对于非匹配串,输出 c。 非匹配串中出现字符P时,输出PP来代替,以示和匹配串的区别。 因此匹配串的输出 ( off, len ) 结果中,不可以出现字符P,以免产生混淆。 本例中,取 (`) 作为前导字符。 2. 对于匹配串,输出为: 前导字符 (`) + 偏移量 (3位,92进制 = 778688) + 匹配长度 (2位,92进制 = 8464) 因此滑动窗大小为778688,最小的匹配长度为 7。 3. 本算法针对JS文件,为简化算法暂不考虑窗口滑动情况(JS文件通常不会大于700K)。对于文件大于778688字节的情况使用本算法会出错。将来可以实现滑动窗口或分段压缩。 4. 本例中为简化算法,将 off 与 len 转换为 92 进制的字符串,并且将低位放在左侧,高位放在右侧。 作者:Hutia Email: [email protected] 转载请注明出处 */ var NC = [], CN = []; NC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*()-=[]\;',./_+{}|:\"<>?".split(""); for (var i=0; i<NC.length; i++) CN[NC[i]] = i; function Lz77Compress(input) { /*LZ77压缩算法 - Hutia - JS版*/ /*变量声明*/ var p = 0; //扫描指针 var lp = 0; //链表查询指针 var len = input.length; //输入字符串的长度 var output = []; //输出 var index = ""; //索引 var head = []; //索引头信息 var prev = []; //位置链表 var match_off = 0; //匹配位置的偏移量 var match_len = 0; //发生匹配的长度 var last_match_off = 0; //上一次匹配位置的偏移量 var last_match_len = 0; //上一次发生匹配的长度 var j = 0; //循环变量 /*循环扫描*/ for (p=0; p<len; p++) { index = input.substring(p, p+7); //取当前字符开始的7个字符作为索引 /*链表维护*/ prev[p] = head[index]; //当前头位置进链表 head[index] = p; //保存现在位置进头信息 /*匹配*/ lp = p; //初始化链表查询指针 match_len = 0; //初始化匹配长度 match_off = 0; //初始化匹配位置 if (prev[lp]) //如果链表上存在上一个匹配 { /*匹配查询*/ while (prev[lp]) //依次查看链表上的每个位置 { lp = prev[lp]; //取出链表上的前一个位置到链表查询指针 for (j=1; j<8464 && lp+j<p; j++) //寻找此位置的最长匹配,匹配长度不能超过8464 (92进制的2个字节长度),也不能超过当前指针位置 { if (input.substring(lp, lp + j) != input.substring(p, p + j)) break; } j--; //计算最长匹配 if (j > 7 && j > match_len) //如果此匹配比已发现的匹配长 { match_len = j; //记录匹配长度 match_off = lp; //记录匹配位置 } } /*匹配处理*/ if (match_len > 7) //如果找到了符合要求的匹配 { if (last_match_len != 0 && last_match_len < match_len) //如果上次匹配存在,且长度没有这次匹配的长度大 { /*懒惰模式*/ output_unmatch(input.charAt(p - 1)); //放弃上次匹配,将字符直接输出 last_match_off = match_off; //记录此次的匹配位置 last_match_len = match_len; //记录此次的匹配长度 } else if (last_match_len != 0) //如果上次匹配存在,且长度比这次匹配的长度大 { /*处理上次的懒惰模式*/ output_match(); //输出上次的匹配 } else //如果上次匹配不存在 { /*懒惰模式*/ last_match_off = match_off; //记录此次的匹配位置 last_match_len = match_len; //记录此次的匹配长度 } } else //如果找不到符合要求的匹配(例如匹配超出当前指针) { if (last_match_len != 0) //如果上次匹配存在 { /*处理上次的懒惰模式*/ output_match(); //输出上次的匹配 } else { output_unmatch(input.charAt(p)); //直接输出当前的字符 } } } else //如果当前不存在匹配 { if (last_match_len != 0) //如果之前发生了匹配 { /*处理上次的懒惰模式*/ output_match(); //输出匹配 } else { output_unmatch(input.charAt(p)); //直接输出当前的字符 } } } //循环扫描结束 /*边界处理*/ if (last_match_len != 0) //如果之前发生了匹配 { /*处理上次的懒惰模式*/ output_match(); //输出匹配 } /*输出*/ return output.join(""); function output_match() { output.push("`"); //输出前缀符 output.push(N2C(last_match_off, 3)); //输出3字节偏移量 output.push(N2C(last_match_len, 2)); //输出2字节匹配长度 p += last_match_len - 2; //移动当前指针到匹配串的末尾(因为懒惰模式,此时 p 指向 last_match_off + 1 的位置,所以应 -2 ) last_match_off = 0; //清空匹配位置 last_match_len = 0; //清空匹配长度 } function output_unmatch(c) { output.push(c == "`" ? "``" : c); //输出未匹配的字符 } } function Lz77Decompress(input) { /*LZ77解压缩算法 - Hutia - JS版*/ /*变量声明*/ var p = 0; //扫描指针 var len = input.length; //输入字符串的长度 var output = []; //输出 var match_off = 0; //匹配位置的偏移量 var match_len = 0; //发生匹配的长度 /*循环扫描*/ for (p=0; p<len; p++) { if (input.charAt(p) == "`") //如果发现前缀标记 { if (input.charAt(p + 1) == "`") //如果是转义前缀 { output.push("`"); //直接输出字符 "`" p++; //指针后移,跳过下一个字符 } else //如果是压缩编码 { match_off = C2N(input.substring(p+1, p+4)); //取出其 1-3 个字符,算出偏移量 match_len = C2N(input.substring(p+4, p+6)); //取出其 4-5 字符,算出匹配长度 output = [].concat(output.join("")); //整理输出内容 output.push(output[0].substring(match_off, match_off + match_len)); //自输出内容的相应偏移量位置取出编码所代表的字符串 p += 5; //指针后移,跳过下5个字符 } } else //如果没有发现前缀标记 { output.push(input.charAt(p)); //直接输出相应的字符 } } /*输出*/ return output.join(""); } /*LZ77解压缩算法 - Hutia - JS / mini 版*/ hutia = function(s){var A="charAt",p=-1,l=s.length,o=[],m,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*()-=[]\;',./_+{}|:\"<>?".split(""),_=[];while(++p<92)_[a[p]]=p;function $(c){var l=c.length,r=0,i=-1;while(++i<l)r+=_[c[A](i)]*Math.pow(92,i);return r;}p=-1;while(++p<l){if(s[A](p)=="`"){if(s[A](p+1)=="`")p++,o.push("`");else{m=$(s.substring(p+1,p+4));o=[].concat(o.join(""));o.push(o[0].substring(m,m+$(s.substring(p+4,p+6))));p+=5;}}else o.push(s.charAt(p));}return o.join("");} function Lz77SelfExtract(s) { return "eval(("+String(hutia)+")(\""+s.replace(/\\/g,"\\\\").replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\"/g,"\\\"")+"\"));"; } function Lz77CompressDefer(input) { /*LZ77压缩算法 - Hutia - JS / Defer 版*/ /*变量声明*/ var p = 0; //扫描指针 var lp = 0; //链表查询指针 var len = input.length; //输入字符串的长度 var output = []; //输出 var index = ""; //索引 var head = []; //索引头信息 var prev = []; //位置链表 var match_off = 0; //匹配位置的偏移量 var match_len = 0; //发生匹配的长度 var last_match_off = 0; //上一次匹配位置的偏移量 var last_match_len = 0; //上一次发生匹配的长度 var j = 0; //循环变量 var callback; //回调函数 this.start = function(fn) { this.start = function(){} callback = fn; run(); } this.status = function () { return p / len; } function run() { var inner_i = 0; /*循环扫描*/ for (; p<len; p++) { if (++inner_i > 400) { return setTimeout(run); } index = input.substring(p, p+7); //取当前字符开始的7个字符作为索引 /*链表维护*/ prev[p] = head[index]; //当前头位置进链表 head[index] = p; //保存现在位置进头信息 /*匹配*/ lp = p; //初始化链表查询指针 match_len = 0; //初始化匹配长度 match_off = 0; //初始化匹配位置 if (prev[lp]) //如果链表上存在上一个匹配 { /*匹配查询*/ while (prev[lp]) //依次查看链表上的每个位置 { lp = prev[lp]; //取出链表上的前一个位置到链表查询指针 for (j=1; j<8464 && lp+j<p; j++) //寻找此位置的最长匹配,匹配长度不能超过8464 (92进制的2个字节长度),也不能超过当前指针位置 { if (input.substring(lp, lp + j) != input.substring(p, p + j)) break; } j--; //计算最长匹配 if (j > 7 && j > match_len) //如果此匹配比已发现的匹配长 { match_len = j; //记录匹配长度 match_off = lp; //记录匹配位置 } } /*匹配处理*/ if (match_len > 7) //如果找到了符合要求的匹配 { if (last_match_len != 0 && last_match_len < match_len) //如果上次匹配存在,且长度没有这次匹配的长度大 { /*懒惰模式*/ output_unmatch(input.charAt(p - 1)); //放弃上次匹配,将字符直接输出 last_match_off = match_off; //记录此次的匹配位置 last_match_len = match_len; //记录此次的匹配长度 } else if (last_match_len != 0) //如果上次匹配存在,且长度比这次匹配的长度大 { /*处理上次的懒惰模式*/ output_match(); //输出上次的匹配 } else //如果上次匹配不存在 { /*懒惰模式*/ last_match_off = match_off; //记录此次的匹配位置 last_match_len = match_len; //记录此次的匹配长度 } } else //如果找不到符合要求的匹配(例如匹配超出当前指针) { if (last_match_len != 0) //如果上次匹配存在 { /*处理上次的懒惰模式*/ output_match(); //输出上次的匹配 } else { output_unmatch(input.charAt(p)); //直接输出当前的字符 } } } else //如果当前不存在匹配 { if (last_match_len != 0) //如果之前发生了匹配 { /*处理上次的懒惰模式*/ output_match(); //输出匹配 } else { output_unmatch(input.charAt(p)); //直接输出当前的字符 } } } //循环扫描结束 /*边界处理*/ if (last_match_len != 0) //如果之前发生了匹配 { /*处理上次的懒惰模式*/ output_match(); //输出匹配 } /*回调输出*/ callback(output.join("")); } //end of run function output_match() { output.push("`"); //输出前缀符 output.push(N2C(last_match_off, 3)); //输出3字节偏移量 output.push(N2C(last_match_len, 2)); //输出2字节匹配长度 p += last_match_len - 2; //移动当前指针到匹配串的末尾(因为懒惰模式,此时 p 指向 last_match_off + 1 的位置,所以应 -2 ) last_match_off = 0; //清空匹配位置 last_match_len = 0; //清空匹配长度 } function output_unmatch(c) { output.push(c == "`" ? "``" : c); //输出未匹配的字符 } } function C2N(c) //将 92 进制字符串(高位在右)转换为 10 进制数字 { var len = c.length; var re = 0; for (var i=0; i<len; i++) { re += CN[c.charAt(i)] * Math.pow(92, i); } return re; } function N2C(n, len) //将 10 进制数字转换为指定长度的 92 进制字符串,高位在右 { var re = []; for (var i=0; i<len; i++) { re[i] = NC[n % 92]; n = n / 92 | 0; } return re.join(""); } </script> </head> <body> <textarea id="txtS"></textarea> <textarea id="txtR"></textarea> <br/> <input type="button" value="Go" id="btn1"> </body> </html>
众所周知,Js这个东西是老外开发的,所有,他没有内置GBK编码的功能,而在中国,大家都操作系统(windows)默认的codepage就是GBK的,所有呢,当中文出现在URL里面发送的时候,浏览器就会进行一次URLEncode!
GBK转码组件 http://www.1kjs.com/lib/widget/gbk/
附件里有个《js 解压缩算法》 用js代码编写的压缩与解压缩算法,deflate压缩,inflate解压