文本編輯器中文字斷行及排版算法研究
袁永福
2013/4/15
文本編輯器是一種非常復雜的圖形軟件,涉及到的很多開發技巧和軟件結構都是傳統的數據庫程序開發中所從未應用的,因此掌握相關技術的人是非常的少的。在其中文字斷行及排版算法是編輯器開發中的核心算法之一。如果沒有掌握這個算法,那只能在開源軟件的基礎上小打小鬧了。
本文就討論一下編輯器中文檔斷行及排版算法。
文字排版大致分為以下幾個步驟:
- 測量各個字符的寬度和高度。[袁永福版權所有]
- 計算文檔容器的客戶區寬度。比如設置的紙張寬度減去左頁邊距和右頁邊距的寬度。這里的文檔容器不僅僅指大的正文區域,還包括單元格、文本框之類的文檔結構。
- 斷行,也就是將各個字符從左到右,從上到下的依次放置在文檔容器中。產生一行行文本,實現一種流式排版。
- 行內排版,也就是在文檔行中進行字符排版,特別是為了完成文檔內容兩邊對齊功能。
- 分頁。
■■測量字符大小
排版的第一步就是計算文檔中各個字符的寬度和高度。筆者是使用C#開發的,因此可以調用System.Drawing.Graphics.MeasureString方法來測量字符的寬度和高度。由於文檔中字符個數很多,比如幾萬個,則一個個測量是非常消耗時間的,為此需要采用很多優化手段來加速測量。[袁永福版權所有]
說到測量字符,就涉及到等寬字體和比例字體的概念了。等寬字體就是使用該字體繪制字符,字符的寬度是一樣的,比如“宋體”,它就是等寬字體,用它來測量和繪制字母“W”和“i”,其寬度是一樣的。比例字體就是使用該字體測量和繪制字符,其寬度是不一樣的,比如“Times new roman”,用它來測量字母“W”和“i”,其寬度是不一樣的。
對於等寬字體,可以事先測量一個字符的寬度,比如“W”,則以后遇到其他字符就使用這個已經測量好的寬度;而對於比例字體,則需要進行實時的測量。
不過一般來說,對於等寬字體和比例字體,中文符號的寬度還是一致的。因此可以實現測量一個中文字符的寬度,以后遇到中文字符就采用這個事先測好的寬度。
這里帶來一個問題,如何判斷一個字符是否為中文字符,那就需要參照GB3212,GBK等計算機字符集的標准來判斷了。一般來說Unicode編碼范圍從19968至40869的字符為中文字符,當然為了進一步的優化,可以知道一些全角符號,它們的寬度也等於中文字符。
不過僅僅依照UNICODE編碼來判斷是否是中文字符是不可靠的。因為一樣的UNICODE字符在不同的字體中其意義可能是不一樣的。[袁永福版權所有]
比如對於字體“Wingdings”,所有的字符在這個字體中完全變味了,就表示一個個特定形狀的符號,判斷是否是中文就毫無意義了;另外對於條碼字體也有這種情況。
最為保險的做法就是直接解析字體二進制文件(擴展名為ttf或ttc),獲得其中的字體輪廓信息,然后根據字符的UNICODE編碼值來計算出字符的寬度,這樣做是最為准確可靠的。筆者猜測Graphics.MeasureString方法內部也可能采用這種方法。不過編輯器自己解析字體二進制文件進行字符測量,繞過底層諸多的調用層次,其速度可以非常的快,可以在幾十毫秒內完成幾萬個字符的測量。[袁永福版權所有]
不過解析字體二進制文件信息還是要花掉不少時間的,比如對於宋體,其字體文件名simsun.ttc,文件大小15MB,含28762個字符輪廓信息。但分析所得的結果信息量很小,只有1424 字節,為此需要將分析結果保存在一個臨時文件中,下次就無需分析這個字體二進制文件了。
■■斷行
測量完字符的大小后,編輯器程序開始在內存中構造排版對象模型,不斷的將字符填充到最后一個文檔行,若文檔行的字符寬度和加上准備添加的字符的寬度大於文檔容器客戶區寬度時,就進行斷行,另起一行開始填充字符。
不過也存在提前斷行的情況。為了盡量保證連續的英文字母字符和阿拉伯數字之間不能出現斷行,這樣會導致同一個邏輯上密切相關的單詞被拆散放在兩行了。因此遇到這種情況需要提前斷行。
為此程序在執行斷行的時候需要進行判斷,如果下一個字符和文檔行中最后幾個字符都是英文字母字符或阿拉伯數字字符時,需要從右到左遍歷最后一個文檔行,將相關字符抽取出來,准備放置在下一行中。[袁永福版權所有]
當然這樣的操作也不是絕對的,比如遇到連續的超級長的“單詞”時,比如100個連續字符“a”,雖然基本上沒有實際意義,但這是一種必需考慮的邊界條件,很容易導致程序運行錯誤。因此在提前斷行時需要進行這樣的判斷,若真的出現這種情況,那就取消提前斷行。
※前置標點和后置標點
不能出現在行尾的符號稱為前置標點,例如“([{·‘“〈《「『【〔〖(.[{£¥”;不能出現在行首的符號稱為后置標點,例如“!),.:;?]}¨·ˇˉ―‖’”…∶、。〃々〉》」』】〕〗!"'),.:;?]`|}~¢”。
比如一個文本行內容為“?張三李四王五【”,這就是一種不和規范的文本行,需要避免這種情況。
在進行文字斷行時,若這個文檔行的最后一個字符是前置標點時,需要進行提前斷行;如果斷行后第一個要排版的字符為后置標點時,也需要進行提前斷行。
在進行斷行的時候,對於段落符號要進行一些特殊處理。段落符號本身是有一定的寬度的,但當文檔行要執行斷行時,參與計算時的寬度就可以當做零了。
在排版的編程實踐中,筆者采用堆棧的方式實現斷行。首先將所有要排版的字符壓入一個堆棧中,然后循環從堆棧中Peek獲得一個字符元素,然后試圖添加到當前文檔行中,若文檔行剩余空間足夠容納新字符,則將該新字符添加到文檔行中,同時堆棧執行Pop操作。若文檔行剩余空間不夠,則不執行Pop操作,新建一個文檔行,從而開始新的循環。如果出現提前斷行,則需要將當前文檔行中的若干個字符元素移出來,並壓入堆棧中等着下一次循環中使用。
當堆棧內容為空時,就跳出循環,完成文檔的斷行操作。[袁永福版權所有]
※停止行
用戶在編輯的時候會頻繁的輸入字符,這就使得程序頻繁的進行文檔排版操作。當文檔內容比較多,比如上萬個字符時,進行整個文檔范圍的字符排版及重新繪制用戶界面可能要花上幾百毫秒的,這樣就導致用戶輸入字符時編輯器反應遲鈍。
為此在用戶編輯錄入的時候,需要進行文檔內容的部分區域的文字排版,而其他區域的排版就不要動了。為此在編程中采用了一種技巧來減輕排版的工作量,筆者稱之為停止行技巧。
在排版前,首先備份文檔容器的文檔行信息。在每完成一個斷行,形成一個新的文檔行時。遍歷備份的文檔行信息,從最后一行開始和新的文檔行內容進行比較,比較內容主要是文檔行中的文檔元素是否完全一致,當然還有一些其他判斷。當新舊兩個文檔行內容一致時,這個舊的文檔行稱為停止行。此時文檔內容斷行提前結束。然后進行新文檔行的行內排版,最后新文檔行和一部分舊的文檔行合並,形成新的文檔排版。這樣就能比較大的降低運行時排版工作量。[袁永福版權所有]
■■行內排版
文字斷行完成后,需要進行行內排版。
文檔行中各個字符的寬度之和不大可能正好等於文檔容器的客戶區寬度。兩者會有空白差。
由於中文字符和英文字符寬度不一樣,對於不等寬字體,各個英文字符、數字字符等寬度還不一樣。使得各個文本行的字符寬度之和是不一樣的,使得各個文檔行右邊緣是參差不齊的。這樣比較嚴重的影響美觀。
為此需要將文檔行的寬度拉長成文檔容器客戶區寬度,由此會額外的制造出不少空白,此時需要將這些空白比較均勻的分攤到各個字符上。此處是比較均勻的分攤,但不是完全均勻,是有一定的分布算法的。
同一行中,字符不是相對孤立的,而且從邏輯上分為一組一組的,對於漢字和標點符號,它們是各自為政,自己組成一組。對於連續的英文字母字符和阿拉伯數字,它們邏輯上是同一組的,一起構成一個完整的單詞,因此同一組之間的字符之間應該是緊密連接在一起,不得拆開。[袁永福版權所有]
為此要分攤由於文字兩邊對齊而造成的額外空間時,首先要對文檔行的字符進行分組,然后將額外的空白平均分攤到字符組上。
例如對於文字“DCWriter電子病歷文本編輯器。”,其分組為“[DCWriter][電][子][病][歷][文][本][編][輯][器][。]”,其中一對方括號之間就是一組字符,這樣就分成11組。如果額外的空白寬度為20個單位,則需要將空白平均分攤到這些字符組上面,最后一組不分攤,於是前面10組分配得到20÷(11-1)=2個單位的空白寬度。在排版時將這10個2單位的空白寬度插入到字符組之間,這樣就能拉長文檔行的寬度正好等於文檔容器的客戶區寬度。
■■分頁
分頁本質上說就是計算分頁線的位置。其過程如下
- 首先計算出標准頁的高度,也就是紙張高度減去上下頁邊距的值,還需要考慮到頁眉頁腳的修正量。
- 設置當前分頁線的位置,也就是上一個分頁線的位置加上標准頁高。
- 遍歷文檔行,若分頁線的位置在文檔行中間,說明該行文字被分割到兩頁中,此時將分頁線的位置向上移動,使得分頁線在當前文檔行的上邊緣和上一個文檔行下邊緣的中間。
- 如此循環,使得所有的文檔頁的高度和大於等於文檔的內容高度。[袁永福版權所有]
在進行分頁時,也需要判斷很多邊界條件,比如當某個文檔行非常高,比如中間放置了一個超高的圖片,使得這個文檔行的高度大於標准頁高,此時就不能隨便移動分頁線的位置了。
另外當文檔中有表格時,則需要深入到表格單元格內部進行修正分頁線位置的操作,這是一種遞歸操作。
在電子病歷業務中有着繼續打印的功能,在筆者的實現中,續打位置實際上就算是一種特殊的分頁線,這樣就能避免在續打時文字被分割打印的情況。
文字斷行和排版算法是非常復雜的,即使筆者經過長期的重構再重構,優化再優化,也還是花費了一萬多行的C#代碼來實現這個功能,而且還有不少地方仍然需要優化。
一些人認為C#無法開發高性能的程序,編輯器這樣程序應該需要用C++開發。筆者經過實踐認為,所謂C#性能不高的說法是不對的,關鍵還是算法。C#程序只是啟動有些慢,運行起來后仍然可以達到很高的性能。[袁永福版權所有]
關於編輯器軟件可以看網站www.dcwriter.cn 。