文本编辑器是一种非常复杂的图形软件,涉及到的很多开发技巧和软件结构都是传统的数据库程序开发中所从未应用的,因此掌握相关技术的人是非常的少的。在其中文字断行及排版算法是编辑器开发中的核心算法之一。如果没有掌握这个算法,那只能在开源软件的基础上小打小闹了。
袁永福 2018-3-9
文本编辑器是一种非常复杂的图形软件,涉及到的很多开发技巧和软件结构都是传统的数据库程序开发中所从未应用的,因此掌握相关技术的人是非常的少的。在其中文字断行及排版算法是编辑器开发中的核心算法之一。如果没有掌握这个算法,那只能在开源软件的基础上小打小闹了。
本文就讨论一下编辑器中文档断行及排版算法。
文字排版大致分为以下几个步骤:
计算文档容器的客户区宽度。比如设置的纸张宽度减去左页边距和右页边距的宽度。这里的文档容器不仅仅指大的正文区域,还包括单元格、文本框之类的文档结构。
断行,也就是将各个字符从左到右,从上到下的依次放置在文档容器中。产生一行行文本,实现一种流式排版。
行内排版,也就是在文档行中进行字符排版,特别是为了完成文档内容两边对齐功能。
■■
测量字符大小
说到测量字符,就涉及到等宽字体和比例字体的概念了。等宽字体就是使用该字体绘制字符,字符的宽度是一样的,比如“宋体”,它就是等宽字体,用它来测量和绘制字母“W”和“i”,其宽度是一样的。比例字体就是使用该字体测量和绘制字符,其宽度是不一样的,比如“Times new roman”,用它来测量字母“W”和“i”,其宽度是不一样的。
对于等宽字体,可以事先测量一个字符的宽度,比如“W”,则以后遇到其他字符就使用这个已经测量好的宽度;而对于比例字体,则需要进行实时的测量。
不过一般来说,对于等宽字体和比例字体,中文符号的宽度还是一致的。因此可以实现测量一个中文字符的宽度,以后遇到中文字符就采用这个事先测好的宽度。
这里带来一个问题,如何判断一个字符是否为中文字符,那就需要参照GB3212,GBK等计算机字符集的标准来判断了。一般来说Unicode编码范围从19968至40869的字符为中文字符,当然为了进一步的优化,可以知道一些全角符号,它们的宽度也等于中文字符。
比如对于字体“Wingdings”,所有的字符在这个字体中完全变味了,就表示一个个特定形状的符号,判断是否是中文就毫无意义了;另外对于条码字体也有这种情况。
不过解析字体二进制文件信息还是要花掉不少时间的,比如对于宋体,其字体文件名simsun.ttc,文件大小15MB,含28762个字符轮廓信息。但分析所得的结果信息量很小,只有1424 字节,为此需要将分析结果保存在一个临时文件中,下次就无需分析这个字体二进制文件了。
■■
断行
测量完字符的大小后,编辑器程序开始在内存中构造排版对象模型,不断的将字符填充到最后一个文档行,若文档行的字符宽度和加上准备添加的字符的宽度大于文档容器客户区宽度时,就进行断行,另起一行开始填充字符。
不过也存在提前断行的情况。为了尽量保证连续的英文字母字符和阿拉伯数字之间不能出现断行,这样会导致同一个逻辑上密切相关的单词被拆散放在两行了。因此遇到这种情况需要提前断行。
当然这样的操作也不是绝对的,比如遇到连续的超级长的“单词”时,比如100个连续字符“a”,虽然基本上没有实际意义,但这是一种必需考虑的边界条件,很容易导致程序运行错误。因此在提前断行时需要进行这样的判断,若真的出现这种情况,那就取消提前断行。
※
前置标点和后置标点
不能出现在行尾的符号称为前置标点,例如“([{·‘“〈《「『【〔〖(.[{£¥”;不能出现在行首的符号称为后置标点,例如“!),.:;?]}¨·ˇˉ―‖’”…∶、。〃々〉》」』】〕〗!"'),.:;?]`|}~¢”。
比如一个文本行内容为“?张三李四王五【”,这就是一种不和规范的文本行,需要避免这种情况。
在进行文字断行时,若这个文档行的最后一个字符是前置标点时,需要进行提前断行;如果断行后第一个要排版的字符为后置标点时,也需要进行提前断行。
在进行断行的时候,对于段落符号要进行一些特殊处理。段落符号本身是有一定的宽度的,但当文档行要执行断行时,参与计算时的宽度就可以当做零了。
在排版的编程实践中,笔者采用堆栈的方式实现断行。首先将所有要排版的字符压入一个堆栈中,然后循环从堆栈中Peek获得一个字符元素,然后试图添加到当前文档行中,若文档行剩余空间足够容纳新字符,则将该新字符添加到文档行中,同时堆栈执行Pop操作。若文档行剩余空间不够,则不执行Pop操作,新建一个文档行,从而开始新的循环。如果出现提前断行,则需要将当前文档行中的若干个字符元素移出来,并压入堆栈中等着下一次循环中使用。
※
停止行
用户在编辑的时候会频繁的输入字符,这就使得程序频繁的进行文档排版操作。当文档内容比较多,比如上万个字符时,进行整个文档范围的字符排版及重新绘制用户界面可能要花上几百毫秒的,这样就导致用户输入字符时编辑器反应迟钝。
为此在用户编辑录入的时候,需要进行文档内容的部分区域的文字排版,而其他区域的排版就不要动了。为此在编程中采用了一种技巧来减轻排版的工作量,笔者称之为停止行技巧。
■■
行内排版
文字断行完成后,需要进行行内排版。
文档行中各个字符的宽度之和不大可能正好等于文档容器的客户区宽度。两者会有空白差。
由于中文字符和英文字符宽度不一样,对于不等宽字体,各个英文字符、数字字符等宽度还不一样。使得各个文本行的字符宽度之和是不一样的,使得各个文档行右边缘是参差不齐的。这样比较严重的影响美观。
为此需要将文档行的宽度拉长成文档容器客户区宽度,由此会额外的制造出不少空白,此时需要将这些空白比较均匀的分摊到各个字符上。此处是比较均匀的分摊,但不是完全均匀,是有一定的分布算法的。
为此要分摊由于文字两边对齐而造成的额外空间时,首先要对文档行的字符进行分组,然后将额外的空白平均分摊到字符组上。
例如对于文字“DCWriter电子病历文本编辑器。”,其分组为“[DCWriter][/电][子][病][历][文][本][编][辑][器][。]”,其中一对方括号之间就是一组字符,这样就分成11组。如果额外的空白宽度为20个单位,则需要将空白平均分摊到这些字符组上面,最后一组不分摊,于是前面10组分配得到20÷(11-1)=2个单位的空白宽度。在排版时将这10个2单位的空白宽度插入到字符组之间,这样就能拉长文档行的宽度正好等于文档容器的客户区宽度。
■■
分页
分页本质上说就是计算分页线的位置。其过程如下
首先计算出标准页的高度,也就是纸张高度减去上下页边距的值,还需要考虑到页眉页脚的修正量。
设置当前分页线的位置,也就是上一个分页线的位置加上标准页高。
遍历文档行,若分页线的位置在文档行中间,说明该行文字被分割到两页中,此时将分页线的位置向上移动,使得分页线在当前文档行的上边缘和上一个文档行下边缘的中间。
在进行分页时,也需要判断很多边界条件,比如当某个文档行非常高,比如中间放置了一个超高的图片,使得这个文档行的高度大于标准页高,此时就不能随便移动分页线的位置了。
另外当文档中有表格时,则需要深入到表格单元格内部进行修正分页线位置的操作,这是一种递归操作。
在电子病历业务中有着继续打印的功能,在笔者的实现中,续打位置实际上就算是一种特殊的分页线,这样就能避免在续打时文字被分割打印的情况。
文字断行和排版算法是非常复杂的,即使笔者经过长期的重构再重构,优化再优化,也还是花费了一万多行的C#代码来实现这个功能,而且还有不少地方仍然需要优化。