乐者为王

Do one thing, and do it well.

批量更新表中某个字段的值到另一个字段的存储过程(MySQL版)

对博客做了一些修改,需要将entries表中的updatetime字段值替换成pubtime字段的值,为此写了一个MySQL存储过程,其中涉及游标的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DELIMITER //

CREATE PROCEDURE sp_batch_replace_field_value_with_another()
BEGIN
    DECLARE p_id INT;
    DECLARE p_pubtime DATETIME;
    DECLARE p_done BOOL;
    DECLARE p_cursor CURSOR FOR SELECT id, pubtime FROM entries;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET p_done = true;

    OPEN p_cursor;
        REPEAT
            FETCH p_cursor INTO p_id, p_pubtime;
            UPDATE entries SET updatetime = p_pubtime WHERE id = p_id;
            UNTIL p_done = true
        END REPEAT;
    CLOSE p_cursor;
END//

DELIMITER ;

第一次在SQLyog中写存储过程,在修改了存储过程后竟然不知道如何保存。点击File -> Save却是保存为另外一个文件,而不是直接保存修改的存储过程。经过多番尝试后发现按F5键就可以了。

com.sybase.jdbc.SybSQLException: The parameter '@user' in the procedure 'sp_adm_chk_lvl' was not declared as an OUTPUT parameter

运行环境:WebLogic 8.16 + Sybase ASE 12.5

问题:The New, Save from File menu are grayed out

分析打印出来的日志文件找到有以下异常出现:

1
2
3
4
5
org.springframework.jdbc.UncategorizedSQLException: CallableStatementCallback
    ; uncategorized SQLException for SQL [{? = call sp_adm_chk_lvl(?, ?, ?, ?, ?, ?, ?)}]
    ; SQL state [ZZZZZ]; error code [276]
    ; The parameter '@user' in the procedure 'sp_adm_chk_lvl' was not declared as an OUTPUT parameter.
    ; nested exception is com.sybase.jdbc.SybSQLException: The parameter '@user' in the procedure 'sp_adm_chk_lvl' was not declared as an OUTPUT parameter.

搜索到一篇出现类似问题的文章 http://www.ibm.com/support/docview.wss?uid=swg21174150 ,其中有这么一段文字:

1
2
3
4
If you call CallableStatement.clearParameters() when the statement has a ?=rpc return value,
we forget that there is a return value and the next time the rpc is executed we send too many
input parameters, and they are all shifted to the right. This problem has been corrected,
and jConnect now has the expected behavior for the clearParameters() method.

因此猜测菜单变灰的问题是由于使用了不正确的JDBC Driver版本。在Weblogic Console发现Connection Pools中Driver Name是com.sybase.jdbc.SybDriver,而正确的设置应该是com.sybase.jdbc2.jdbc.SybDriver,修改Driver Name后New, Save菜单项果然可以正常使用了。

双色球(Union Lotto)模拟摇奖器

双色球基本规则:

  1. 彩票投注区分为红色球号码区和蓝色球号码区;
  2. 每注投注号码由6个红色球号码和1个蓝色球号码组成。红色球号码从1-33中选择;蓝色球号码从1-16中选择;
  3. 摇奖时先摇出6个红色球号码,再摇出1个蓝色球号码,摇出的红色球号码按从小到大的顺序和蓝色球号码一起公布。

双色球模拟摇奖器代码:

1
2
3
4
5
6
7
# union_lotto.rb
red_balls = Array.new(33) {|i| i + 1}
blue_balls = Array.new(16) {|i| i + 1}
1.upto(6) do
  print red_balls.delete_at(rand(red_balls.length)), ' '
end
puts '- ' + blue_balls.delete_at(rand(blue_balls.length)).to_s

Cannot delete or update a parent row: a foreign key constraint fails

在SQLyog中Import Data或Empty Table时,出现:

1
2
Error No. 1451
Cannot delete or update a parent row: a foreign key constraint fails

查看官方文档Setting FOREIGN_KEY_CHECKS to 0 can also be useful for ignoring foreign key constraints during LOAD DATA and ALTER TABLE operations.知道这可能是MySQL在InnoDB中设置了foreign key关联,造成无法更新或删除数据。可以通过设置FOREIGN_KEY_CHECKS变量来避免这种情况。

1
SET FOREIGN_KEY_CHECKS = 0;

JavaScript中的undefined

JavaScript有6种类型,undefined、null、boolean、number、string和object。其中,null类型只有一个值null。undefined类型也只有一个值,即undefined,当声明的变量没有被赋值的时候,该变量的默认值就是undefined。

1
var foo;

上面这行代码声明了变量foo,没有赋值。脚本引擎在处理代码的时候会给该变量赋予值undefined。实际上,该行代码等价于

1
var foo = undefined;

可以用下面的代码测试:

1
2
3
4
var foo;
alert(foo == undefined);    // true
alert(foo === undefined);    // true
alert(typeof(foo));    // undefined

JavaScript另一个有趣的方面是在使用变量之前不必声明。例如:

1
2
3
var foo = "Hello ";
bar = foo + "world!";
alert(bar);    // Hello world!

变量bar并没有用var运算符定义,这里只是使用了它,就像已经声明过它一样。脚本引擎遇到未声明过的标识符时,用该变量名创建一个全局变量,并将其初始化为指定的值(这里是foo + "world")。这是该语言的便利之处,不过如果不能紧密跟踪变量,这样做也很危险。考虑下面的代码:

1
2
3
4
5
var foo;
// Make sure this variable isn't defined
//var bar;
alert(foo);
alert(bar);

执行的结果是:

alert(bar)出现错误是因为bar被创建成全局变量后没有被初始化,还处在未初始化(uninitialized)状态。但是,typeof运算符并不真正区分这两种值。考虑下面的代码:

1
2
3
4
5
var foo;
// Make sure this variable isn't defined
//var bar;
alert(typeof(foo));    // undefined
alert(typeof(bar));    // undefined

前面的代码对两个变量的输出都是“undefined”,即使变量bar在使用前没有被声明。

当函数无明确返回值时,返回的也是值undefined,如下所示:

1
2
3
function foobar() { }
alert(foobar() == undefined);    // true
alert(foobar() === undefined);    // true

ECMAScript认为undefined是从null派生出来的,所以把它们定义为相等的:

1
alert(null == undefined);    // true

尽管这两个值相等,但它们的含义不同。undefined是声明了变量但未赋值时赋予该变量的值,null则用于表示尚未存在的对象(但从技术上来说,null仍然是原始值)。如果要区分两者,要使用===或typeof运算符。

CSS盒模型

平面示意图

3D示意图(原图出自:hicksdesign)

Flash示意模型

W3C DOM - {setAttribute()}

setAttribute(string name, string value):增加一个指定名称和值的新属性,或者把一个现有的属性设定为指定的值。

setAttribute()的差异

我们经常需要在JavaScript中给Element动态添加各种属性,这可以通过使用setAttribute()来实现,这就涉及到了浏览器的兼容性问题。

1
2
var obj = document.getElementById("obj");
obj.setAttribute("onclick", "javascript:alert('This is a test!');");

这里利用setAttribute指定Element的onclick属性,简单,很好理解。但是IE不支持,IE并不是不支持setAttribute这个函数,而是不支持用setAttribute设置某些属性,例如对象属性、集合属性、事件属性,也就是说用setAttribute设置style和onclick 这些属性在IE中是行不通的。为达到兼容各种浏览器的效果,可以用点符号法来设置Element的对象属性、集合属性和事件属性。

1
2
3
4
document.getElementById("obj").className = "fruit";
document.getElementById("obj").style.cssText = "color: #00f;";
document.getElementById("obj").style.color = "#00f";
document.getElementById("obj").onclick = function () { alert("This is a test!"); }

关于class和className

class属性在W3C DOM中扮演着很重要的角色,但由于浏览器差异性仍然存在。使用setAttribute("class", vName)语句动态设置element的class属性在Firefox中是行的通的,在IE中却不行。因为使用IE内核的浏览器不认识“class”,要改用“className”;同样,Firefox也不认识“className”。所以常用的方法是二者兼备:

1
2
element.setAttribute("class", vName);
element.setAttribute("className", vName);    // for IE

cellspacing和cellpadding

虽然在CSS中存在与cellpadding和cellspacing这两个属性等价的样式属性padding和border-spacing。然而,浏览器对这些样式属性支持的不一致,有时仍会使用cellpadding和cellspacing来调整表格的间距。不过,在Firefox中有效的setAttribute("cellpadding", value)到了IE下就不行了,必须改成cellPadding才可以(注意:p要大写)。幸好Firefox也支持setAttribute("cellPadding", value)。所以二者兼容的代码是:

1
2
element.setAttribute("cellSpacing", value);
element.setAttribute("cellPadding", value);

JSON in JavaScript

英文原文:http://www.json.org/js.html

JavaScript是一门通用编程语言,被作为页面脚本语言引入Netscape Navigator。它仍被广泛地认为是Java的一个子集,但它不是。它是一门有着类C语法soft objects的类Scheme语言,JavaScript在ECMAScript Language Specification, Third Edition中被标准化。

JSON是JavaScript对象字面量表示法(object literal notation)的一个子集。因为JSON是JavaScript的一个子集,所以它可以毫不费力地被用在这门语言中。

1
2
3
4
5
6
var myJSONObject = {"bindings": [
        {"ircEvent": "PRIVMSG", "method": "newURI", "regex": "^http://.*"},
        {"ircEvent": "PRIVMSG", "method": "deleteURI", "regex": "^delete.*"},
        {"ircEvent": "PRIVMSG", "method": "randomURI", "regex": "^random.*"}
    ]
};

在这个例子中,一个含有单个成员“bindings”的对象被创建,该成员包含一个含有三个对象的数组,每个对象含有“ircEvent”,“method”和“regex”三个成员。

成员可以通过点或下标运算符检索。

1
myJSONObject.bindings[0].method    // "newURI"

你可以使用eval函数将一段JSON文本转换成一个对象,eval函数会调用JavaScript编译器。因为JSON是JavaScript的一个真子集(a proper subset),编译器将正确地解析文本并产生一个对象结构。文本必须被包裹在括号内,以免在JavaScript的语法中的歧义上犯错误。

1
var myObject = eval('(' + myJSONtext + ')');

eval函数是非常快的。然而,它可以编译和执行任何JavaScript程序,所以可能会有安全问题。当源代码是可信的与合格的的时候才可以使用eval函数。使用JSON语法分析器则安全的多。在基于XMLHttpRequest的Web应用中,通讯只被允许朝着提供该页面的同源的方向,所以它是可信的。但它可能不是合格的。如果服务器在它的JSON编码里不是严格的,或者它没有严谨地验证所有的输入,然后它可能交付无效的JSON文本,它们可能携带危险的脚本。eval函数将会执行这些脚本,释放恶意。

为抵御这些,应该使用JSON语法分析器。JSON语法分析器将只识别JSON文本,拒绝所有的脚本。在提供本地JSON支持的浏览器里,JSON语法分析器也比eval快的多。预计本地JSON支持将会被包含在下一个ECMAScript标准中。

1
var myObject = JSON.parse(myJSONtext, reviver);

可选的reviver参数是一个函数,它会被最终结果的各个层面上的每个键和值调用。每个值会被reviver函数的返回结果替换。这可以被用来将通用对象改编成为伪类实例,或者被用来将date字符串转化成Date对象。

1
2
3
4
5
6
7
8
9
10
myData = JSON.parse(text, function (key, value) {
    var type;
    if (value && typeof value === 'object') {
        type = value.type;
        if (typeof type === 'string' && typeof window[type] === 'function') {
            return new (window[type])(value);
        }
    }
    return value;
});

JSON stringifier则作相反的工作,它将JavaScript数据结构转换为JSON文本。JSON不支持循环的数据结构,所以注意不要把循环的结构交给JSON stringifier。

1
var myJSONText = JSON.stringify(myObject, replacer);

如果stringify方法看到一个对象包含toJSON方法,它会调用该方法,并字符串化返回的值。这允许一个对象去确定其自己的JSON表示。

stringify~~原文是stringifier,疑似拼错~~方法可以接受一个可选的字符串数组。这些字符串被用于选取那些将会被包含在JSON文本中的属性。

stringify~~原文是stringifier,疑似拼错~~方法可以接受一个可选的replacer函数。该函数会在结构中每个值的toJSON方法(如果有)之后被调用。它将每个键和值作为参数传递,并且this将会被绑定到持有键的对象上。返回的值会被字符串化。

没有在JSON中表示的值(例如函数和undefined)被排除在外。

非有限的数字被替换为null。要替换成其它的值,你可以使用像这样的replacer函数:

1
2
3
4
5
6
function replacer(key, value) {
    if (typeof value === 'number' && !isFinite(value)) {
        return String(value);
    }
    return value;
}

给JSON.parse一个相应的reviver可以还原这些。

The open source code of a JSON parser and JSON stringifier is available. When minified it is less than 2.5K.

集成P6Spy到Spring后没有产生日志文件的问题

网上关于集成P6Spy到Spring的帖子说:在Spring的配置文件中添加:

1
2
3
<bean id="dataSource" class="com.p6spy.engine.spy.P6DataSource" destroy-method="close">
    <constructor-arg ref="dataSourceTarget" />
</bean>

然后将原来dataSource定义的id值改为dataSourceTarget:

1
2
3
4
5
6
<bean id="dataSourceTarget" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${driver}" />
    <property name="url" value="${url}" />
    <property name="username" value="${username}" />
    <property name="password" value="${password}" />
</bean>

但是照着这样做了以后却发现没有生成日志文件。

下载P6Spy的源代码进行阅读后找到了问题所在。打开P6Util.java文件,在classPathFile()方法中有这样一条语句:

1
fp = classLoadPropertyFile(Thread.currentThread().getContextClassLoader().getResource(file))

getResource(file)用来读取spy.properties配置文件,file的值就是spy.properties的文件路径。我的syp.properties放在了C:\Tomcat 5.5\webapps\netlink\WEB-INF\classes目录下,所以它返回的值是C:/Tomcat%205.5/webapps/netlink/WEB-INF/classes/spy.properties,Tomcat 5.5中的空格被编码成了%20。然后在classLoadPropertyFile()方法中就直接使用该值来创建File对象。可想而知,肯定创建失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static File classLoadPropertyFile(java.net.URL purl) {
    try {
        if (purl != null) {
            // modified by jayakumar for JDK 1.2 support
            //return new File(purl.getPath());
            return new File(getPath(purl));
            // end of modification
        }
    } catch (Exception e) {
        // we ignore this, since JDK 1.2 does not suppport this method
    }
    return null;
}

在classPathFile()方法中通过判断fp.exists()来决定是否返回日志文件的路径,既然上面创建File对象失败了,那fp.exists()肯定不可能为true了,日志文件也就不可能被创建了。

解决问题的方法超简单,把Tomcat 5.5中的空格去掉就行了。

图象的半影调和抖动技术

在介绍本讲内容之前,先提出一个问题?普通的黑白针式打印机能打出灰度图来吗?如果说能,从针式打印机的打印原理来分析,似乎是不可能的。因为针打是靠撞针击打色带在纸上形成黑点的,不可能打出灰色的点来;如果说不能,可是我们的确见过用针式打印机打印出来的灰色图象。到底是怎么回事呢?你再仔细看看那些打印出来的所谓的灰色图象,最好用放大镜看。你会发现,原来这些灰色图象都是由一些黑点组成的,黑点多一些,图象就暗一些;黑点少一些,图案就亮一些。下面这几张图就能说明这一点。

图1. 用黑白两种颜色打印出灰度效果

图中最左边的是原图,是一幅真正的灰度图,另外三张图都是黑白二值图。容易看出,最左的那幅和原图最接近。由二值图象显示出灰度效果的方法,就是我们今天要讲的半影调(halftone)技术,它的一个主要用途就是在只有二值输出的打印机上打印图象。我们介绍两种方法:图案法和抖动法。

图案法(patterning)

图案法是指灰度可以用一定比例的黑白点组成的区域表示,从而达到整体图象的灰度感。黑白点的位置选择称为图案化。在具体介绍图案法之前,先介绍一下分辨率的概念。计算机显示器,打印机,扫描仪等设备的一个重要指标就是分辨率,单位是dpi(dot per inch),即每英寸点数,点数越多,分辨率就越高,图象就越清晰。让我们来计算一下,计算机显示器的分辨率有多高。设显示器为15英寸(指对角线长度),最多显示1280 * 1024个点。因为宽高比为4:3,则宽有12英寸,高有9英寸,则该显示器的水平分辨率为106dpi,垂直分辨率为113.8dpi。一般的激光打印机的分辨率有300dpi * 300dpi的,有600dpi * 600dpi的,720dpi * 720dpi。所以打出来的图象要比计算机显示出来的清晰的多。扫描仪的分辨率要高一些,数码相机的分辨率更高。

言归正传,前面讲了,图案化使用图案来表示像素的灰度,那么我们来做一道计算题。假设有一幅240 * 180 * 8bit的灰度图,当用分辨率为300dpi * 300dpi的激光打印机将其打印到12.8 * 9.6英寸的纸上时,每个像素的图案有多大?这道题很简单,这张纸最多可以打(300 * 12.8) * (300 * 9.6) = 3840 * 2880个点,所以每个像素可以用(3840 / 240) * (2880 / 180) = 16 * 16个点大小的图案来表示,即一个像素256个点。如果这16 * 16的方块中一个黑点也没有,就可以表示灰度256,有一个黑点,就表示灰度255,依次类推,当都是黑点时,表示灰度0,这样,16 * 16的方块可以表示257级灰度。比要求的8bit共256级灰度还多了一个,所以上面的那幅图的灰度级别完全能够打印出来。

这里有一个图案构成的问题,即黑点打在哪里?比如说,只有一个黑点时,我们可以打在正中央,也可以打16 * 16的左上角。图案可以是规则的,也可以是不规则的。一般情况下,有规则的图案比随即图案能够避免点的丛集,但有时会导致图象中有明显的线条。如图2中,2 * 2的图案可以表示5级灰度,当图象中有一片灰度为的1的区域时,如图3所示,有明显的水平和垂直线条。

图2. 2 * 2的图案

图3. 规则图案导致线条

如果想存储256级灰度的图案,就需要256 * 16 * 16的二值点阵,占用的空间还是相当可观的。有一个更好的办法是:只存储一个整数矩阵,称为标准图案,其中的每个值从0到255。图象的实际灰度和阵列中的每个值比较,当该值大于等于灰度时,对应点打一黑点。下面举一个25级灰度的例子加以说明。

图4. 标准图案举例

图4中,左边为标准图案,右边为灰度为15的图案,共有10个黑点,15个白点。其实道理很简单,灰度为0时全是黑点,灰度每增加1,减少一个黑点。要注意的是,5 * 5的图案可以表示26种灰度,灰度25才是全白点,而不是24。下面介绍一种设计标准图案的算法,是由Limb在1969年提出的。以一个2 * 2的矩阵开始,通过递归关系有,其中2n * 2n是阵列中元素的个数,Un是一个2n * 2n 的方阵,所有元素都是1。根据这个算法,可以得到,为16级灰度的标准图案。M3(8 * 8阵)比较特殊,称为Bayer抖动表。M4是一个16 * 16的矩阵。根据上面的算法,如果利用M3,一个像素要用8 * 8的图案表示,则一幅N * N的图将变成8N * 8N大小。如果利用M4,就更不得了,变成16N * 16N了。能不能在保持原图大小的情况下利用图案化技术呢?一种很自然的想法是:如果用M2阵,则将原图中每8 * 8个点中取一点,即重新采样,然后再应用图案化技术,就能够保持原图大小。实际上,这种方法并不可行。首先,你不知道这8 * 8个点中找哪一点比较合适,另外,8 * 8的间隔实在太大了,生成的图象和原图肯定相差很大,就象图1最右边的那幅图一样。我们可以采用这样的做法:假设原图是256级灰度,利用Bayer抖动表,做如下处理if (g[y][x] >> 2) > bayer[y & 7][x & 7] then 打一白点 else 打一黑点。其中,x,y代表原图的像素坐标,g[y][x]代表该点灰度。首先将灰度右移两位,变成64级,然后将x,y做模8运算,找到Bayer表中的对应点,两者做比较,根据上面给出的判据做处理。

我们可以看到,模8运算使得原图分成了一个个8 * 8的小块,每个小块和8 * 8的Bayer表相对应。小块中的每个点都参与了比较,这样就避免了上面提到的选点和块划分过大的问题。模8运算实质上是引入了随机成分,这就是我们下面要讲到的抖动技术。

下面的图5就是利用这个算法,使用M3(Bayer抖动表)阵得到的,图6是使用M4阵得到的,可见两者的差别并不是很大,所以一般用Bayer表就可以了。

图5. 利用M3抖动生成的图

图6. 利用M4抖动生成的图

图案法源程序

抖动法(dithering)

让我们考虑更坏的情况:即使使用了图案化技术,仍然得不到要求的灰度级别。举例说明:假设有一幅600 * 450 * 8bit的灰度图,当用分辨率为300dpi * 300dpi的激光打印机将其打印到8 * 6英寸的纸上时,每个像素可以用(2400 / 600) * (1800 / 450) = 4 * 4个点大小的图案来表示,最多能表示17级灰度,无法满足256级灰度的要求。可有两种解决方案:1.减小图象尺寸,由600 * 450变为150 * 113;2.降低图象灰度级,由256级变成16级。这两种方案都不理想。这时,我们就可以采用抖动法的技术来解决这个问题。其实刚才给出的算法就是一种抖动算法,称为规则抖动(regular dithering)。规则抖动的优点是算法简单;缺点是图案化有时很明显,这是因为取模运算虽然引入了随机成分,但还是有规律的,另外,点之间进行比较时,只要比标准图案上点的值大就打白点,这种做法并不理想,因为,如果当标准图案点的灰度值本身就很小,而图象中点的灰度只比它大一点儿时,图象中的点更接近黑色,而不是白色。一种更好的方法是将这个误差传播到邻近的像素。下面介绍的Floyd-Steinberg算法就采用了这种方案。

假设灰度级别的范围从b(black)到w(white),中间值t为(b + w) /2,对应256级灰度,b = 0, w = 255, t = 127.5;设原图中像素的灰度为g,误差值为e,则新图中对应像素的值用如下的方法得到:

1
2
3
4
5
6
7
8
9
if g > t then
    打白点
    e = g - w
else
    打黑点
    e = g - b
    3 / 8 * e 加到右边的像素
    3 / 8 * e 加到下边的像素
    1 / 4 * e 加到右下方的像素

算法的意思很明白,以256级灰度为例,假设一个点的灰度为130,在灰度图中应该是一个灰点。由于一般图象中灰度是连续变化的,相邻像素的灰度值很可能与本像素非常接近,所以该点及周围应该是一片灰色区域。在新图中,130大于128,所以打了白点,但130离真正的白点255还差的比较远,误差e = 130 - 255 = -125 比较大。将3 / 8 * (-125)加到相邻像素后,使得相邻像素的值接近0而打黑点。下一次,e又变成正的,使得相邻像素的相邻像素打白点,这样一白一黑一白,表现出来刚好就是灰色。如果不传递误差,就是一片白色了。再举个例子,如果一个点的灰度为250,在灰度图中应该是一个白点,该点及周围应该是一片白色区域。在新图中,虽然e = -5也是负的,但其值很小,对相邻像素的影响不大,所以还是能够打出一片白色区域来。这样就验证了算法的正确性。其它的情况可以自己推敲。图7是利用Floyd-Steinberg算法抖动生成的图

图7. 利用Floyd-Steinberg算法抖动生成的图

Floyd-Steinberg算法的源代码

bmp2txt(bmp to txt)

在讲图案化技术时,我突然想到了一个非常有趣的应用,那就是bmp2txt。如果你喜欢上BBS(电子公告牌系统),你可能想做一个花哨的签名档。瞧,这是我好朋友Casper的签名档,胖乎乎的,是不是特别可爱?

图8. Casper的签名档

你仔细观察一下,就会发现,这是一幅全部由字符组成的图,因为在BBS中只能出现文字的东西。那么,这幅图是怎么做出来的呢?难道是自己一个字符一个字符拼出来的。当然不是了,有一种叫bmp2txt的应用程序(2的发音和“to”一样,所以如此命名),能把位图文件转换成和图案很相似的字符文本。是不是觉得很神奇?其实原理很简单,用到了和图案化技术类似的思想:首先将位图分成同样大小的小块,求出每一块灰度的平均值,然后和每个字符的灰度做比较,找出最接近的那个字符,来代表这一小块图象。那么,怎么确定字符的灰度呢?做下面的实验就明白了。

打开Notepad,输入字符“1”,选定该字符,使其反色。按Alt+PrintScreen键拷贝窗口屏幕。打开Paintbrush,粘贴,然后把图放到最大(*8),打开“查看” -> “缩放” -> “显示网格”菜单,如下图所示:

图9. 字符“1”的灰度

这时数数字符“1”用了几个点?是22个。我想你已经明白了,字符的灰度和它所占的黑色点数有关,点越少,灰度值越大,空格字符的灰度最大,为全白,因为它一个黑点也没有;而字符“W”的灰度值就比较低了。每个字符的面积是8 * 16(宽 * 高),所以一个字符的灰度值可以用如下的公式计算(1 - 所占的黑点数 / (8 * 16)) * 255。下面是可显示的字符,及对应的灰度,共有95个。这可是casper辛辛苦苦整理出来的呦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static char ch[95] = {
    ' ',
    '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\\',
    'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']',
    'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ''',
    'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
    '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '|',
    'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}',
    'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"',
    'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?'
};

static int gr[95] = {
    0,
    7, 22, 28, 31, 31, 27, 32, 22, 38, 32, 40, 6, 12, 20, 38, 32, 26, 20, 24, 40,
    29, 24, 28, 38, 32, 32, 26, 22, 34, 24, 44, 33, 32, 32, 24, 16, 6, 22, 26, 22,
    26, 34, 29, 35, 10, 6, 20, 14, 22, 47, 42, 34, 40, 10, 35, 21, 22, 22, 16, 14,
    26, 40, 39, 29, 38, 22, 28, 36, 22, 36, 30, 22, 22, 36, 26, 36, 25, 34, 38, 24,
    36, 22, 12, 12, 26, 30, 30, 34, 39, 42, 41, 18, 18, 22
};

bmp2txt源程序

其实利用图案化技术,还可以实现更有趣的应用,如下面这幅图,你仔细看看,贝多芬的头像是由许多个音乐符号组成的。

图10. 贝多芬的头像