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