袁創:文本編輯器中文字斷行及排版算法研究

文本編輯器是一種很是複雜的圖形軟件,涉及到的不少開發技巧和軟件結構都是傳統的數據庫程序開發中所從未應用的,所以掌握相關技術的人是很是的少的。在其中文字斷行及排版算法是編輯器開發中的核心算法之一。若是沒有掌握這個算法,那隻能在開源軟件的基礎上小打小鬧了。算法

 本文就討論一下編輯器中文檔斷行及排版算法。數據庫

文字排版大體分爲如下幾個步驟:編程

  1. 測量各個字符的寬度和高度。[袁永福版權全部]
  2. 計算文檔容器的客戶區寬度。好比設置的紙張寬度減去左頁邊距和右頁邊距的寬度。這裏的文檔容器不只僅指大的正文區域,還包括單元格、文本框之類的文檔結構。
  3. 斷行,也就是將各個字符從左到右,從上到下的依次放置在文檔容器中。產生一行行文本,實現一種流式排版。
  4. 行內排版,也就是在文檔行中進行字符排版,特別是爲了完成文檔內容兩邊對齊功能。
  5. 分頁。

 ■■測量字符大小編輯器

  排版的第一步就是計算文檔中各個字符的寬度和高度。筆者是使用C#開發的,所以能夠調用System.Drawing.Graphics.MeasureString方法來測量字符的寬度和高度。因爲文檔中字符個數不少,好比幾萬個,則一個個測量是很是消耗時間的,爲此須要採用不少優化手段來加速測量。[袁永福版權全部]性能

  說到測量字符,就涉及到等寬字體和比例字體的概念了。等寬字體就是使用該字體繪製字符,字符的寬度是同樣的,好比「宋體」,它就是等寬字體,用它來測量和繪製字母「W」和「i」,其寬度是同樣的。比例字體就是使用該字體測量和繪製字符,其寬度是不同的,好比「Times new roman」,用它來測量字母「W」和「i」,其寬度是不同的。字體

  對於等寬字體,能夠事先測量一個字符的寬度,好比「W」,則之後遇到其餘字符就使用這個已經測量好的寬度;而對於比例字體,則須要進行實時的測量。優化

  不過通常來講,對於等寬字體和比例字體,中文符號的寬度仍是一致的。所以能夠實現測量一箇中文字符的寬度,之後遇到中文字符就採用這個事先測好的寬度。編碼

  這裏帶來一個問題,如何判斷一個字符是否爲中文字符,那就須要參照GB3212,GBK等計算機字符集的標準來判斷了。通常來講Unicode編碼範圍從19968至40869的字符爲中文字符,固然爲了進一步的優化,能夠知道一些全角符號,它們的寬度也等於中文字符。code

  不過僅僅依照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單位的空白寬度插入到字符組之間,這樣就能拉長文檔行的寬度正好等於文檔容器的客戶區寬度。

 ■■分頁

  分頁本質上說就是計算分頁線的位置。其過程以下

  1. 首先計算出標準頁的高度,也就是紙張高度減去上下頁邊距的值,還須要考慮到頁眉頁腳的修正量。
  2. 設置當前分頁線的位置,也就是上一個分頁線的位置加上標準頁高。
  3. 遍歷文檔行,若分頁線的位置在文檔行中間,說明該行文字被分割到兩頁中,此時將分頁線的位置向上移動,使得分頁線在當前文檔行的上邊緣和上一個文檔行下邊緣的中間。
  4. 如此循環,使得全部的文檔頁的高度和大於等於文檔的內容高度。[袁永福版權全部]

  在進行分頁時,也須要判斷不少邊界條件,好比當某個文檔行很是高,好比中間放置了一個超高的圖片,使得這個文檔行的高度大於標準頁高,此時就不能隨便移動分頁線的位置了。

  另外當文檔中有表格時,則須要深刻到表格單元格內部進行修正分頁線位置的操做,這是一種遞歸操做。

   在電子病歷業務中有着繼續打印的功能,在筆者的實現中,續打位置實際上就算是一種特殊的分頁線,這樣就能避免在續打時文字被分割打印的狀況。

   文字斷行和排版算法是很是複雜的,即便筆者通過長期的重構再重構,優化再優化,也仍是花費了一萬多行的C#代碼來實現這個功能,並且還有很多地方仍然須要優化。

  一些人認爲C#沒法開發高性能的程序,編輯器這樣程序應該須要用C++開發。筆者通過實踐認爲,所謂C#性能不高的說法是不對的,關鍵仍是算法。C#程序只是啓動有些慢,運行起來後仍然能夠達到很高的性能。[袁永福版權全部]

文本編輯器是一種很是複雜的圖形軟件,涉及到的不少開發技巧和軟件結構都是傳統的數據庫程序開發中所從未應用的,所以掌握相關技術的人是很是的少的。在其中文字斷行及排版算法是編輯器開發中的核心算法之一。若是沒有掌握這個算法,那隻能在開源軟件的基礎上小打小鬧了。

 本文就討論一下編輯器中文檔斷行及排版算法。

文字排版大體分爲如下幾個步驟:

  1. 測量各個字符的寬度和高度。[袁永福版權全部]
  2. 計算文檔容器的客戶區寬度。好比設置的紙張寬度減去左頁邊距和右頁邊距的寬度。這裏的文檔容器不只僅指大的正文區域,還包括單元格、文本框之類的文檔結構。
  3. 斷行,也就是將各個字符從左到右,從上到下的依次放置在文檔容器中。產生一行行文本,實現一種流式排版。
  4. 行內排版,也就是在文檔行中進行字符排版,特別是爲了完成文檔內容兩邊對齊功能。
  5. 分頁。

 ■■測量字符大小

  排版的第一步就是計算文檔中各個字符的寬度和高度。筆者是使用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單位的空白寬度插入到字符組之間,這樣就能拉長文檔行的寬度正好等於文檔容器的客戶區寬度。

 ■■分頁

  分頁本質上說就是計算分頁線的位置。其過程以下

  1. 首先計算出標準頁的高度,也就是紙張高度減去上下頁邊距的值,還須要考慮到頁眉頁腳的修正量。
  2. 設置當前分頁線的位置,也就是上一個分頁線的位置加上標準頁高。
  3. 遍歷文檔行,若分頁線的位置在文檔行中間,說明該行文字被分割到兩頁中,此時將分頁線的位置向上移動,使得分頁線在當前文檔行的上邊緣和上一個文檔行下邊緣的中間。
  4. 如此循環,使得全部的文檔頁的高度和大於等於文檔的內容高度。[袁永福版權全部]

  在進行分頁時,也須要判斷不少邊界條件,好比當某個文檔行很是高,好比中間放置了一個超高的圖片,使得這個文檔行的高度大於標準頁高,此時就不能隨便移動分頁線的位置了。

  另外當文檔中有表格時,則須要深刻到表格單元格內部進行修正分頁線位置的操做,這是一種遞歸操做。

   在電子病歷業務中有着繼續打印的功能,在筆者的實現中,續打位置實際上就算是一種特殊的分頁線,這樣就能避免在續打時文字被分割打印的狀況。

   文字斷行和排版算法是很是複雜的,即便筆者通過長期的重構再重構,優化再優化,也仍是花費了一萬多行的C#代碼來實現這個功能,並且還有很多地方仍然須要優化。

  一些人認爲C#沒法開發高性能的程序,編輯器這樣程序應該須要用C++開發。筆者通過實踐認爲,所謂C#性能不高的說法是不對的,關鍵仍是算法。C#程序只是啓動有些慢,運行起來後仍然能夠達到很高的性能。[袁永福版權全部]

相關文章
相關標籤/搜索