邱明冉



編者按:每個操作系統在設計之初,都會考慮如何在終端上顯示文字,而且都不約而同地采用了字體這一方式來顯示豐富的文字形態。字體對于操作系統的重要性不言而喻,從本期開始,我們將探討系統中字體的顯示和渲染原理,以及對字體的優化和應用。
一位日常使用Linux系統的同學說:“Mac的字體顯示對高分屏有優化,Linux的字體顯示對所有屏幕都有優化。”作為一個需要整天面對屏幕上密密麻麻文字的學生,聽到這句話,筆者心動了。但是筆者裝好雙系統之后,并沒有感受到Linux的字體顯示比Windows 10的顯示有進步。為了刨根究底,我們需要從計算機是如何顯示文字的開始探索……
● 像素——顯示的基本單元
在討論計算機如何顯示字體之前,先介紹一下VGA是如何進行顯示的。
以640*480@60Hz的VGA顯示規格為例,電子槍不斷地重復著從左上角掃描到右下角,再回到左上角的過程,為了能夠顯示640*480像素的有效區域,電子槍的掃描范圍相當于800*525像素,多出來的“無效掃描”就是在電子槍掃描無法顯示的邊框處和返回行首列過程中發生的(如圖1)。
于是,為了讓連接VGA的顯示器能夠以640*480@60Hz工作,必須設計一個電路,使得開發板能夠在正確的時候(當電子槍掃描到800*525中有效的640*480時)通過VGA傳遞當前要打印的像素的顏色。這個電路還可以檢測當前掃描到的像素坐標。
VGA顯示器接收到正確的垂直同步信號和RGB值才能正常工作。既然可以控制每一個像素的顏色,那么也就可以在屏幕上顯示文字了(如圖2)。
● 從像素到文字
在計算機科學中,抽象是一個重要的理念。如果每次顯示文字的時候都要在像素點層次工作,那就太煩瑣了。于是計算機科學家們想到,將整個640*480像素的屏幕劃分成一個個8像素寬、16像素高的切片,每個切片顯示一個ASCII字符。預先定義好每個ASCII字符在這8*16像素內如何顯示,要顯示哪個字符的時候,就加載定義好的顯示方式來顯示即可。
這種抽象出來的方法可以應用到多個方面,在游戲場景制作時也可以這樣分解。Unity引擎將游戲畫面分解成一塊塊“地磚”,設計師只需要預先設計幾種地磚,通過鋪地磚就可以快速完成游戲場景設計(如圖3)。
用現在的編程思維來講,就是預先定義好一個一維數組character,每個字符就是數組的一個元素。數組元素的大小為8*16=128位,每一位表示這8*16個像素中某一個像素是黑還是白,那么“A”的顯示方式和存儲方式就如圖4所示。
在電路的具體實現上,開發板將這些信息保存到ROM(只讀內存)中,供VGA使用。這種字體,稱為點陣字體(Bitmap Font,直譯為“位圖字體”)。在后來的DOS系統中,顯示模式分為圖形模式和文本模式,圖形模式下通過直接寫顯存可以對每一個像素的顏色進行控制,而文本模式下通過寫顯存只能控制哪一個位置輸出哪個字符以及字符的顏色。
由于計算機主板上的ROM容量較小又比較寶貴,一般只存儲最基本的128個ASCII碼字符,在這些ASCII字符之外的文字和符號(如漢字和特殊符號)要想顯示出來,就必須采用其他方法,如將點陣信息儲存在硬件插卡或磁盤文件中,形成字庫。
在中文DOS系統中,不僅要解決字庫的加載,還要解決如何在屏幕上顯示這些文字。經歷過漢卡之后,曾經的UCDOS、CCDOS等優秀的漢字系統,最終采用“直接寫屏”(調用BIOS的10H中斷在圖形模式下直接操作顯存地址)技術以圖形方式在屏幕上“畫”出漢字。在純粹的西文DOS環境中,要想顯示出漢字是一個不小的挑戰。字庫文件最后成了一致看好的最終解決方案。
點陣字體大小固定,但是支持整數倍放大。然而這樣放大后就像位圖一樣,會有明顯的鋸齒。所以,為了滿足不同大小的需要,一種字體需要提供好幾個版本。圖5是8*16的字體放大成16*32的大小(左),與本來就是16*32大小字體的比較(右),可以看出,用放大方法得到的字體,有明顯的鋸齒,并且最初的不對稱也被放大。
即使是同一種字體,在不同大小下看起來也像是不同字體一樣。例如,當字體較大時,襯線體可以顯示出自己的襯線,而字體較小時,因為像素點數量的限制,這種細節只能被舍棄。我們在命令提示符下查看同一點陣字體不同大小時的差別。8*16大小的Terminal字體,可以看到明顯的襯線以及字體橫細豎粗的特點(如上頁圖6)。
而6*12大小的Terminal字體,和8*16長寬比相等,但是沒有襯腳,筆畫粗細均勻,看上去和前者完全不是一種字體(如圖7)。
8*18大小的Terminal字體也跟8*16大小的Terminal字體完全不同(如圖8)。
● 從文字到像素
隨著圖形界面的普及,用戶對一種字體提供多種字號的需求不斷增長。點陣字體的設計效率不如人意,尤其是對非字母語言的文字而言。于是,設計師想到用數學方程來描述筆畫,把字符分隔成若干關鍵點,用光滑的曲線連接,通過計算就可以得出同一種字體在各種不同的字號下應該如何顯示。這就是矢量字體(Vector Font,也稱輪廓字體)。
Photoshop的鋼筆工具可以繪制貝塞爾曲線,這種曲線依賴于數學方程,不受畫布像素的限制,顯得非常平滑(如圖9)。
但是當使用它在畫布上繪圖并填充時,就會發現繪制區域的圖形依然受到畫布像素的制約,每一個像素只能有一種顏色,平滑的邊緣會變成鋸齒狀(如圖10)。同理,通過數學函數設計的矢量字體,當最終呈現在顯示器上的時候,也會受到顯示器像素的制約,在字號較小的時候,顯示的效果可能會很差。
例如,字母“e”的矢量設計圖(如圖11左側),它如何在顯示器上顯示呢?顯示器上的每個像素就是一個小格子,我們可以用初等數學中常用的二值化進行處理:如果一個像素一半以上面積被字體覆蓋,那么這個像素就用來顯示字體,否則這個像素不顯示字體。圖11右側就是轉換過程。
當字體較小時,為了避免像素太少引起矢量字體的嚴重失真,Windows的做法是對小號字體直接使用原始的點陣字體,不進行矢量渲染。不過微軟對Windows 10系統默認的微軟雅黑字體的處理是例外,全字號使用ClearType亞像素渲染技術,所以有人覺得雅黑字體顯示效果較好。
● 抗鋸齒技術
上面所說的非黑即白填充法只能渲染出粗糙的字體效果。在Windows XP之前的Windows系統,微軟就采用這樣的做法來顯示矢量字體。要改進顯示效果,可以根據理想的字型在此像素所覆蓋的面積比例,賦予像素不同的灰度,靠近字體邊緣的地方表現為深灰到淺灰不等的顏色(如上頁圖11),人眼會將它轉換為字體的輪廓。這種通過過渡色減輕字體邊緣鋸齒感的技術,就是抗鋸齒技術(如上頁圖12)。
字體抗鋸齒在日常使用中效果如何呢?這里以圖形軟件開發常用的Qt框架為例來演示。這是窗口中的一個按鈕,按鈕文字字號是50,圖13和圖14展示了抗鋸齒的效果。
或者我們也可以用14號字,截圖后放大觀察區別(如圖15、圖16)。從圖15中可以看到,它使用了紅、藍、棕等顏色而不是灰色作過渡色(詳見后文“亞像素”部分)。
抗鋸齒在字體的日常使用感受中影響還是很大的,尤其是在字號較小的情況下。開啟了抗鋸齒之后,不但字形得到了更好的還原,顯示效果在大部分情況下也有明顯提高。目前的系統也都默認開啟了字體抗鋸齒(如圖17)。
● 字體微調
有時候在小字號下,開啟了抗鋸齒,字體糊成一團,不易辨別,顯示效果反而變差了。為了解決抗鋸齒導致的字體邊緣模糊問題,人們想到通過輕微調整字形,使其盡可能與像素點貼合,以避免大量使用過渡像素導致觀感模糊,從而生成清晰易讀的文本。這就是字體微調(hinting,直譯為“提示”)。
圖18中維基百科的圖片很好地詮釋了“字體微調”。上下兩行中上一行沒有字體微調,下一行有字體微調。
微軟十分重視字體微調,認為顯示字體的時候應該針對顯示屏做一定的優化,使之適應較低精度的像素分布,獲得清晰效果,方便用戶閱讀。而蘋果認為,在顯示屏上所顯示的字體應該和印刷出來的成品效果接近,即使這樣會導致字體看上去有些模糊。二者字體渲染理念的不同也就產生了不同的效果(如圖19)。
字體微調使文字清晰易讀的同時,一定程度上扭曲了字體,甚至可能導致比較復雜的文字會顯示錯誤的字形。把Windows 10桌面(屏幕縮放100%)下默認大小的字體放大來看,可以看到為了使筆畫對比度提升、顯示更清晰,字體微調甚至將“真”字里面的三橫減去了一橫(如圖20)。
在關閉字體微調效果的Linux XFCE桌面上同樣的文字,筆畫沒丟,但顯示效果比較模糊,讀起來眼睛很累(如圖21)。
上面兩圖的比較是在不同系統不同字體上進行的,沒有控制變量,不夠嚴謹。我們來看看Linux XFCE桌面下Noto Serif Regular字體(大小為10號)開啟微調的效果(如圖22、圖23),可以看到大寫的E、H、R等字母明顯更銳利了,小寫g下面的圈從3*2的長方形變成了3*3的正方形,這也是字體微調之后與原字形產生的差別。
除了對字形進行微調,有時還會對字距進行微調,以達到更自然的顯示效果(如圖24)。
● 亞像素渲染
在液晶顯示屏上,一個像素是由紅、綠、藍三個長條形亞像素構成的,LCD可以單獨控制每一個子像素,這個特性正好可以用來提高字體精度。白色字體只有離得非常近才能觀察到白色是由RGB三種顏色混合而成的。
亞像素渲染正是利用了LCD顯示屏像素的特點,將字體水平方向的精細度提高到原先的三倍(如圖25),在字號較小的情況下尤其有效。
圖26形象展示了亞像素渲染是如何提高水平精度的,圖左側把每個像素分解成RGB三個亞像素,實際上肉眼只能看出圖右側的效果。
采用亞像素渲染后,如果僅僅是在計算機屏幕上放大(如使用XFCE桌面的ALT+滾輪進行全局縮放,或者Snipaste、QQ截圖等截圖工具自帶的放大鏡),看到的只是字體輪廓花花綠綠的整塊整塊大小的像素,而且一定是“白底黑字左橙右青,黑底白字左青右橙”。只有用微距相機對準屏幕拍攝,然后放大查看照片,才能發現字體輪廓附近像素的RGB亞像素亮暗不同(如圖27)。
亞像素渲染加入了顏色信息,圖28左側輪廓邊緣亞像素分別是R(亮)G(一般)B(暗),所以左側像素整體表現出的顏色是大部分紅+小部分綠=偏紅的黃色。
除了水平RGB亞像素渲染,Linux還支持其他種類的亞像素渲染(當然也可以關閉亞像素渲染),以適應各種不同的顯示設備。字體渲染的高度可定制性,就是同學口中“Linux的字體顯示對所有屏幕都有優化”的含義吧。而筆者使用的屏幕完全符合微軟“不甚高端”的預期,已經在Windows 10字體渲染策略下取得了較好的顯示效果,所以才沒有感受到Linux在字體渲染上的優勢……