#js基于html5中canvas的字模提取工具
**(附js生成声音的方式、获取系统支持字体方式、字模显示及其他)**
[上回书说到](../Matrix67_LCD_GB2312_HZK16_other/detail.html "Matrix67的LCD电子公告牌、GB2312-80编码、HZK16字模及其他"),由于[Matrix67的LCD公告牌](http://www.matrix67.com/ "Matrix67的主页")我翻箱倒柜捯饬字模……当时我在网上搜索了一下,令我吃惊的是,市面上大部分字模提取工具居然是收费的……坑爹呀!这太有讽刺意味了!话说想当年用的winTC(Turbo C for win,免费软件)都自带一个生成字模的小工具滴说……而且会用的字模这个东西的,大部分都是相关“挨踢人士”,能用这个东西,自然就能自己写个啊,反正也不是多先进的东西……所以,我就因此励志图强,咱要自力更生,艰苦奋斗!
话说本来字模提取工具应该用C语言写的……我在网上搜索过,大部分提倡的方式是使用系统API将某个字以某字体画出来,然后读像素值……因为系统字体多是矢量字体,要自行绘制字体有些麻烦,所以直接调用系统API会容易一些……我猜测使用windowsAPI/MFC的字模提取程序流程可能是这样的:
>设置好长宽,创建位图(BITMAP),并获得其设备描述句柄(HDC),通过此句柄获得逻辑画刷(BRUSH),用画刷整个刷新背景(FillRect),然后创建逻辑字体(CreateFont),并关联到设备描述句柄(SelectObject),开始写字(TextOut),然后通过设备描述句柄获得相关位置的像素(Get Pixel)并通过像素值生成字模……
好吧,说了这么半天,其实今天的程序不是C语言的……原因是本人虽说对winAPI有所了解,但是已经好长时间不接触了,功能退化了,开发效率必然很低,再加上使用C语言写winAPI程序最大的特点就是代码繁杂,通过上面的描述相信各位已经很清楚了……所以我们要独辟蹊径……
所以,书归正传,html5是个好东西啊,特别是里面的canvas……在我的印象里,如果html系列有能够支持绘图的标准,那么它基本就已经“无敌”了……后来我到图书馆翻阅了各种书籍,得知了VRML、MathML和SVG,还有IE的VML……尽管我知道,它理论上可以“无敌”了……但是我清楚,这并不是理论上我想要的东西,尽管他们使得解决方式开始出现……
后来canvas出现了,我知道这才是我真正想要的东西,尽管它现在在某些方面的功能还值得商榷,但是这一新特性已经足够让我们兴奋了……可惜我还是有点欲求不满,我想要让html系列的协议能够通过js支持生成声音,然后我继续到图书馆翻书,结果发现了VoiceML这么个东西,挺有意思的,这是用来制作声音用户界面(VUI相对于GUI而存在)的东西,当然,它与问题无关,问题依旧木有解决……
直到后来,我开始研究[JS1K](http://js1k.com/ "JS1K")中的一些代码,因为我发现有些程序是带声音的,而且按照JS1K的标准,按理说不会是嵌入性的声音媒体……由此我发现了“`data:XXXX/XXXX;base64,……`”这个东西不仅可以编码一个图片,还可以编码声音,甚至其他你想得到的格式,当然,要浏览器或者相关插件支持,好吧,这个东西用来生成质量不高的声音还行,如果想生成太悦耳的声音不仅需要各种声学和编码功底,还需要浏览器客户端有很强的计算能力……
在JS1K中找到的关于生成声音的某一种代码[Tetris with sound](http://js1k.com/2010-first/demo/730 "JS1K Tetris with sound")([JS1K \#1第三名](http://js1k.com/2010-first/ "JS1K #1 第三名"),written by[@sjoerd_visscher](http://twitter.com/sjoerd_visscher "sjoerd_visscher's twitter"))整理后是这样的,大家可以听听看:
```JavaScript
M=Math;
C=12;
f=[];
R=[];
A='charCodeAt';
S='slice';
for(P=0;P<96;){
//这个注释掉的K的赋值是原乐曲,我改成了下边的乐曲,貌似也不错……
//k="/SN;__/NK;OL/QN;__/OL;NK4L@@_C4_G@OL4SO@__4QN@OL3NB?_G3_K?OL/QN;__/SK;__4OL@__4LC@_G4LC@_G4_C@_G"[A](P);
k="ABCDEFGHIJKLMNOPQRSTUVWXYZZYXWVUTSRQPONMLKJIHGFEDCBA"[A](P);
D="\0\0";
for(j=0;k<95&&j<1e4;){
v=M.max(-1e4,M.min(1e4,1e6*M.sin(j*M.pow(2,k/C)/695)))/M.exp(j++/5e3);
D+=String.fromCharCode(v&255,v>>8&255)
}
R[P++]=new Audio("data:audio/wav;base64,UklGRgAAAABXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YSBO"+btoa(D))
}
var t=0;
setTimeout(sound,500);
function sound(){
R[t++].play();
if(t<52)setTimeout(sound,500);
}
```
Chrome和Opera下效果都不错,Firfox下效果就有点差劲了……
好吧,说言归正传,上面彻底跑题跑远了……这次彻底言归正传吧……canvas提供了获取和操作像素的方式……这意味着什么啊?我觉得这意味着使用JS实现通过编码进行图像处理成为可能呀!
让Chrome成为一个操作系统这是google的终极目标呀……这使我想到了emacs(编辑器之神),一个伪装成编辑器的操作系统……
另外,这让我想到的就是通过canvas+人工神经网络算法再通过一系列的训练,JS就可以破译验证码了有木有……这个有机会的童鞋可以试试……
其实屡次跑题的原因是单就提取字模真没什么好说的……因为总体流程与C语言下是类似的,总体思路都是先画出来,再读像素值,组合成字模……使用JS+canvas的实现的流程可能是这样的:
>创建canvas,设置canvas的长宽,设置文字的对齐方式,设置字号和字体,然后获得内容描述句柄,通过描述句柄填充背景颜色(fillRect),写字(fillText),获取图像数据(getImageData),读像素值,组合成字模……
一种可能的代码:
```JavaScript
var bitArr=new Array();
for(var i=0;i<8;i++)bitArr[i]=(0x80>>i);//初始化位数组
var canvas=document.createElement("canvas");//创建canvas
var ctx=canvas.getContext("2d");//获得内容描述句柄
var fontSize=16;
var font="宋体";
var bs=Math.ceil(fontSize/8);//每行占字节数
canvas.width=fontSize;
canvas.height=fontSize;
ctx.textAlign="left";
ctx.textBaseline="top";
ctx.font=fontSize+"px '"+font+"'";
var outStr="unsigned char zm["+(bs*fontSize)+"]={"+getzm("爱")+"};";
document.write(outStr);
function getzm(c){//获取一个字符的字模的过程
ctx.fillStyle="#000000";
ctx.fillRect(0,0,fontSize,fontSize);//涂背景
ctx.fillStyle="#FFFFFF";
ctx.fillText(c,0,0);//写字
var data=ctx.getImageData(0,0,fontSize,fontSize).data;//获取图像
var zm=new Array(bs*fontSize);
for(var i=0;i<zm.length;i++)zm[i]=0;//初始化字模数组
for(var i=0;i<fontSize;i++)//读像素值组成字模数组
for(var j=0;j<fontSize;j++)
if(data[i*fontSize*4+j*4])zm[parseInt(j/8)+i*bs]+=bitArr[j%8];
var outStr="";//将字模数组转化为十六进制形式
for(var i=0;i<zm.length-1;i++)outStr+=toHex(zm[i])+",";
outStr+=toHex(zm[i]);
return outStr;
}
function toHex(num){//将一个数字转化成16进制字符串形式
return num<16?"0x0"+num.toString(16).toUpperCase():"0x"+num.toString(16).toUpperCase();
}
```
以上代码验证了通过canvas获取字模的可行性……确实是没神马好说的……(此处仅仅是说明代码,具体实现的示例链接将在后面贴出)
此处要特别注意的是,当字号不是8的倍数的时候是怎么处理的……根据我所知的点阵字库的处理方式是,每一行的最后一个字节的低位补零……这样使得每一行都是整字节……
那么每行所占的字节数便是`parseInt((fontSize+7)/8)`或`Math.ceil(fontSize/8)`,一个字模所占的字节数便是`parseInt((fontSize+7)/8)*fontSize`或`Math.ceil(fontSize/8)*fontSize`了(`parseInt`是为了取整,当然,更准确的用法应该是`Math.floor`,呃,个人习惯太差了)……
但是更多的,我们想增加其自定制特性,其他很多自定制特性没神马好说的,说到获取系统支持的字体列表,以便用户自定制选取,问题就来了……
由于安全原因,js无法获得系统支持的字体列表……那么js要获得系统列表必须借助于其他插件……网上看到的最多的js获取系统支持的字体列表的代码是这样的:
```HTML
<html>
<head>
<meta http-equiv="Content-Language" content="zh-cn">
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>获得系统字体示例(仅支持IE)</title>
<script><!--
function changeFont(v){
document.body.style.fontFamily="'"+v+"'";
}
function getAllFonts(){
var dlgHelper=document.getElementById("dlgHelper");
var fontArr=new Array();
//此处要特别注意:索引值从1开始,而不是从0开始!!
for(var i=1;i<dlgHelper.fonts.count;i++)
fontArr.push(dlgHelper.fonts(i));
return fontArr.sort();
}
function showFonts()
{
var fontList=getAllFonts();
var outStr="选择字体:<select onchange='changeFont(this.value)'>";
for(var i=0;i<fontList.length;i++)
outStr+="<option value='"+fontList[i]+"'>"+fontList[i]+"</option>";
outStr+="</select><br />系統共有"+fontList.length+"种字体,如下:<br />";
for (var i=1;i<fontList.length;i++)
outStr+="<font face='"+fontList[i]+"'>"+fontList[i]+"</font><br>\n";
document.getElementById("outDiv").innerHTML=outStr;
}
// --></script>
</head>
<body onload="showFonts()">
<object id="dlgHelper" classid="clsid:3050f819-98b5-11cf-bb82-00aa00bdce0b" width="0px" height="0px"></object>
Font Test,字体测试!<br />
<div id="outDiv"></div>
</body>
</html>
```
很遗憾的是,这个控件仅支持IE……而IE当前对于html5的支持是糟糕的,这就意味着,我们根本不能用这种方式来获取系统支持字体列表……
不过我通过运行上面那个程序,发现了几种有意思的字体:`windings`、`windings 2`、`windings 3`、`symbol、bookself symbol`、`sshlinedraw`(暂时不知道这几种字体是否是所有win操作系统都默认支持的……)
再有一种广泛而常用的方式是使用flash来获取系统支持字体……因为flash就当前来看,是不愁得到支持的……在网上搜了一下,比较多的用flash获取系统字体并显示的方法是这样的:
```ActionScript
import flash.text.Font;
var fontList:Array = Font.enumerateFonts(true);
fontList.sortOn("fontName", Array.CASEINSENSITIVE);
for each(var font:Font in fontList)trace(font.fontName);
```
好吧,这是基于AS3的……我的flash工具太落后了,还是几年前的Micromedia flash 8.0,还不支持AS3……好吧,咱用AS2……
那就是这样的:
```ActionScript
var font_array:Array = TextField.getFontList();
font_array.sort();
trace("You have "+font_array.length+" fonts currently installed");
trace("--------------------------------------");
for (var i = 0; i<font_array.length; i++)
trace("Font #"+(i+1)+":\t"+font_array[i]);
```
好了,下一步就是与js嫁接了……参考[某老外的文章](http://rel.me/2008/06/26/font-detection-with-javascript-and-flash/ "font detection with javascript and flash"),并参考flash8自带的帮助文档……
我了解到flash是通过内置的`flash.external.ExternalInterface`类来解决与js互相调用的问题……那么flash的代码可能是这个样子的:
```ActionScript
import flash.external.ExternalInterface;
//声明js可以使用getAllFonts这个函数名来调用本flash中的getAllFonts()方法
ExternalInterface.addCallback("getAllFonts", this, getAllFonts);
function getAllFonts():Array{
var font_array:Array = TextField.getFontList();
font_array.sort();
return font_array;
}
ExternalInterface.call("show");//调用js中的show方法
```
那么js中就要实现show方法了,可能的代码:
```JavaScript
function getMovie(movieName) {
var isIE = navigator.appName.indexOf("Microsoft") != -1;
return (isIE) ? window[movieName] : document[movieName];
}
function getFontList() {
return getMovie("fontListFlash").getAllFonts();
}
function show(){
var fonts=getFontList();
var selectStr="<select onchange='chooseFont(this.value)'>";
for(var i=0;i<fonts.length;i++)
selectStr+="<option value='"+fonts[i]+"'>"+fonts[i]+"</option>";
selectStr+="<select>";
document.getElementById("outDiv").innerHTML=selectStr;
}
function chooseFont(v){
document.body.style.fontFamily="'"+v+"'";
}
```
当然,为了防止出现`undefined`的情况,这些函数的声明位置最好在flash插入位置之前……
好吧,我承认我在插入flash的时候出现了一些不兼容的情况……不过……功夫不负有心人,我终于找到了兼容的插入flash的方法:
```HTML
<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="14" height="14">
<param name="quality" value="high">
<param name="bgcolor" value="green">
<param name="allowFullScreen" value="false">
<param name="swLiveConnect" value="true">
<param name="allowScriptAccess" value="sameDomain">
<param name="wmode" value="transparent">
<embed type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" width="14" height="14" bgcolor="red" name="fontListFlash" src="fontlist.swf" flashvars="" style="border:1px solid black;"></embed>
</object>
```
当然,其实最好的方法是使用`flashObject`……不过我在想自己写点小程序的时候特别不喜欢别人的东西……所以还是这么引入比较放心……
好吧,字模获取大约就这么点东西了,下一步就是将生成的字模显示出来了……字模显示的方法很多啊,可以再创建一个canvas然后在上面按字模描点……
但是我更喜欢的还是使用纯字符来描述一个点阵……这就比较像当年在telnet版BBS上流行的字符画……而且还可以修改图元,使得文字有不同的纹理……
但是,在此时,我遇到了一个纠结问题……因为要显示字模,必须要得知字模的字号……但是我不想让用户输入,我想通过字模本身的信息求出字号来,如字模所占的字节数……最麻烦的问题就这么产生了……
就这个问题我询问了两位硕士,一位博士,终于验证了一个经验公式在实际应用中的正确性,但是至今没法理论证明其正确性……
最近一直在讨论关于复杂数学能力提高,会伴随着简单数学能力逐渐下降的问题,还由此引出了我一次买油条的故事:
>那天早上没做早饭,突然听到街上卖油条的吆喝声,俺娘说,今天早上不做早饭了,大家吃油条吧,然后差我去卖油条,当时卖油条的还剩下整整一大包油条,我想了想,正好我们全家四口人吃,我就全包了吧,称了称一共17斤,然后油条当时是一块八一斤……然后我给了五十整,他给我找完钱,我没数就装口袋里了……然后我就一边往回走一边算,一直算到把油条吃完了都没算出来那个卖油条的到底应该找给我多少钱……其实我到现在都不知道应该找多少钱……几年前新闻上说某清华学生毕业后回家炸油条,我觉得我上完大学就算炸油条都算不过账来……
这就像是专家解决生产线上的空肥皂盒问题用的是远红外感应装置、自动化高精度控制以及机械手臂,而普通农民工的解决方式确是一个大风扇……(其实这个故事的结论是,不管文化水平多高,能吹才是硬道理……)
看来有时候太复杂的东西还不如现实经验更管用……所以说,神马理论证明都比不上一个经验公式啊……想当年薛定谔就是靠经验把薛定谔方程写出来的;想当年麦克斯韦推导电磁波的时候除了有很强的数学功底之外,当时还有很多经验成分;普朗克发表量子学说的时候,当时那就纯粹是一个经验学说;而德布罗意的物质波就更不用说了……
好吧,言归正传,开始的时候我询问第一位硕士,他的思路是
>设`x`为字号,`m`为最后一个字节空余的位数,`b`为字模所占的字节数
>那么可以列等式:
>$$x(x+m)=8b,(m<8;x,b,m \in N^+)$$
>这样可以根据一元二次方程根的表达式,负根约去,得x定有一个正根$$\frac{-m+\sqrt{m^2+32b}}{2}$$
>根据m的范围`0≤m≤7`,然后根据高阶无穷小规律,此根随m的增大而减小(m趋向于+∞时,x趋向于0)
>当`m=0`时有最大值$$\sqrt{8b}$$
>当`m=7`时有最小值$$\frac{-7+\sqrt{49+32b}}{2}$$
>那么x取区间>$$[\frac{-7+\sqrt{49+32b}}{2},\sqrt{8b}]$$中的整数就可以了……
我借助数学工具算了一晚上,算到了这么个结果:
- 当` 0 < b < 9/50 `时,区间中最多有一个整数
- 当` 9/50 < b < 25/18 `时,区间中至少有1个至多有2个整数
- 当` 25/18 < b < 18 `时,区间中至少有2个至多有3个整数
- 当` b > 18 `时(此时字号为`12`),区间中至少有3个至多有4个整数
对于汉字字模来说,一般最小的字号是`16px`,也就是说,采取这种方式得到的结果是完全不确定的……太坑爹了……
于是我又去询问某博士,他在十几分钟的时间里,给了我一个分段方程:
- 当`x%8 = 0`的时候,`x^2 = 8b`
- 当`x%8 != 0`的时候,`x(x+7-x%8) = 8b`
好吧,我承认这个方程很直观……以及从理论验证了其可求解性……我也承认通过遍历的方式能够把数求出来……
但是……毫无用处呀……因为我们要找`x关于b的表达式`,而不是`b关于x的表达式`……正向求解当然容易,但问题是我们要逆向求解……
好吧,我又问了第二个硕士……在她的启发下,我终于找到了一个更为直观的表达式`x*ceil(x/8)=b`……
我晕,我这才似曾相识的想到了上面程序中的这条语句:`var bs=Math.ceil(fontSize/8);//每行占字节数`
坑爹啊……太坑爹了……就感觉自己跟白忙活了一圈又转回去的感觉……好吧,我们来看看函数`y=x*ceil(x/8)`的函数图象吧:
![函数图像](plot.png "函数图像")
从图象上看……y与x是一一对应的,也直观上证明了它的逆函数是存在的……但是这不证明它的逆函数可以用表达式显式的写出来……
好吧,不跟大家绕圈子了,还是跟大家说我凑出来的经验公式吧:`x=b/ceil(sqrt(b/8))`……我通过遍历证明在正常使用范围内这个公式得到的结果是正确的……
但是现在以本人的数学能力无法证明这个式子在理论上的正确性,请求看到这篇文章的高人给出提示……好了,最麻烦的问题解决了,那么显示字模的可能代码如下:
```JavaScript
function showZm(zm){//zm为字模数组
var zm,tc0="□",tc1="■";//0填充图元和1填充图元
var bs=Math.ceil(Math.sqrt(zm.length/8));//每行占字节数
fontSize=zm.length/bs;
var zs="";
for(var i=0;i<fontSize;i++){
for(var j=0;j<fontSize;j++)zs+=((zm[parseInt(j/8)+i*bs]>>(7-j%8))&1)?tc1:tc0;
if(i!=fontSize-1)zs+="<br />";
}
document.writeln(zs);
}
```
好吧,我我承认在现实实现过程中,为了实现方便,很不厚道很邪恶的使用了`eval函数`……
最后还是给出示例链接吧:[字模提取工具及字模显示工具示例程序](http://1157.huaying1988.com/getZM.html "字模提取工具及字模显示工具示例程序"),[相关的flash源码](http://1157.huaying1988.com/fontlist.fla "相关的flash源码")
在`google chrome 15.0.874.121 m`,`Firegfox 8.0.1`,`Opera 11.51`下测试通过……
不过有意思的是,我在生成并显示“楹”字的`20px`宋体的字模的时候,得到了下面的结果:
![”楹“字的显示效果](ying.png "”楹“字的显示效果")
不过可以确定的是,这是字体本身或者系统在绘制文字的时候出现的问题,而并非获取或显示字模程序的问题,不过这点变形并不影响什么,大可以放心使用……
另外刚才说到将0填充以及1填充换成自己喜欢的,将改变文字纹理,比如说0填充改成“..”,1填充改成“❤”,结果将是这样的:
![”花“字的显示效果](hua.png "”花“字的显示效果")
此处使用两个字符的原因是达到高度上的等宽……如果不这样,想达到等宽效果请自行调整字体、字间距和行高……
最后说说说怎么通过这个工具生成的字模集生成二进制点阵汉字库吧……在上面给出的链接中,默认的字符集其实就是上回书说到的`GB2312`的所有字符……
好吧,说白了它还是那个老生常谈的程序生成的:
```C
#include <stdio.h>
int main(){
int i,j;
for(i=0xa1;i<0xf8;i++)
for(j=0xa1;j<0xff;j++)
printf("%c%c",i,j);
return 0;
}
```
那么点“`→`”按钮生成的字模集就可以直接生成`GB2312`的点阵汉字库了……相关的生成方式十分简单……
首先要生成`C语言`格式的字符数组,由于程序本身是自定制的,可以通过修改参数直接生成:
>字串前导设为“`unsigned char zm[]={`”,字模间分隔符设置为“`,`”,字串后导设置为“`};`”,字模前导置空, 字节间分隔符设置为“`,`”,字模后导置空……
>然后点击“`→`”就生成`C语言`格式的字符数组格式的字模集了……
然后生成`GB2312`点阵汉字库的`C语言`程序相当简单,如下:
```C
#include <stdio.h>
int main(){
unsigned char zm[]={
//………………
//………………
};//把刚才生成的字模集直接粘过来吧!
int fontSize=16,num=8178;//字号,总字数
int total=((fontSize+7)/8)*fontSize*num;//数组中总字节数
printf("字节总数:%d\n",total);
FILE* f=fopen("myHZK.bin","wb");
if(f==NULL){
printf("打开文件失败!\n");
return -1;
}
size_t size=fwrite(zm,sizeof(char),total,f);
printf("写入%d个字节\n",size);
fclose(f);
return 0;
}
```
运行后生成`myHZK.bin`文件,并输出以下:
```
字节总数:261696
写入261696个字节
```
呃……按理说不会有什么差错,还是验证一下吧,可以再拿出上一次写的读点阵库的程序来验证一下:
```C
#include <stdio.h>
int main(){
FILE * f=fopen("myHZK.bin","rb");
int i=0,j=-1;
char zm[32];
int q=0,w=0;
printf("var hzk=[\n");
while(!feof(f)){
++j;
fread(zm,32,sizeof(char),f);
q=j/94+1;w=j%94+1;
if((q>9&&q<16)||q>87)continue;
printf("[");
for(i=0;i<31;i++)
printf("0x%02x,",zm[i]&0xff);
printf("0x%02x],/*%02d%02d %c%c*/\n",zm[31]&0xff,q,w,j/94+0xa1,j%94+0xa1);
}
printf("]");
fclose(f);
return 0;
}
```
呃,本人稍微验证了几个,貌似木有神马问题……好了,大约就这些了……再说说今后可能的打算吧……
如果可能的话,有机会再做个获取图模小工具也不错呵(不过估计是木有机会了)……关于上面提到的使用js来破解验证码的程序有可能也可以钻研一下呵(不过估计是木有可能了)……
另外在网上看到的:[每一个姓都是一朵花](http://www.topit.me/album/377567 "每一个姓都是一朵花"),跟我那个万花规有点像呵……挺有启发性的……有空咱也做个(不过估计是木有空了)……