瀏覽器如何渲染文本
瀏覽器是我們最常用的軟件之一,文本又是網(wǎng)頁中最主要的元素,在瀏覽器顯示文本的過程中有許多有趣的細(xì)節(jié),值得展開來講講,或許能減少一些誤解。這 是一個比較粗略的,概括性的介紹,盡可能不涉及過多的技術(shù)細(xì)節(jié)和具體實現(xiàn),而立足于給 Web 開發(fā)者和設(shè)計師提供一些正確的概念。
下面的介紹主要根據(jù)我對 WebKit 和 Gecko (Firefox) 的印象來談,其他的瀏覽器也大致相同,如有闕漏之處歡迎指出。
當(dāng)瀏覽器收到來自 Web 服務(wù)器的網(wǎng)頁數(shù)據(jù)之后,第一步是要把它解碼成可以閱讀的文本,因為歷史原因,不同區(qū)域和語言的網(wǎng)頁可能會使用不同的編碼方式,而瀏覽器判斷編碼主要是依據(jù)以下方法:
- Web 服務(wù)器返回的 HTTP 頭中的 Content-Type: text/html; charset= 信息,這一般有最高的優(yōu)先級;
- 網(wǎng)頁本身 meta header 中的 Content-Type 信息的 charset 部分,對于 HTTP 頭未指定編碼或者本地文件,一般是這么判斷;
- 假如前兩條都沒有找到,瀏覽器菜單里一般允許用戶強(qiáng)制指定編碼;
- 部分瀏覽器 (比如 Firefox) 可以選擇編碼自動檢測功能,使用基于統(tǒng)計的方法判斷未定編碼。
編碼確定后,網(wǎng)頁就被解碼成了 Unicode 字符流,可以進(jìn)行進(jìn)一步的處理,比如 HTML 解析了,不過我們這里跳過 HTML/XML 解析的細(xì)節(jié),單講得到了解析后的文本元素之后該怎么處理。
因為我們得到的文本可能是很多種語言混雜的,里面可能有中文、有英文,它們可能要用不同的字體顯示;也可能有阿拉伯文、希伯來文這種從右到左書寫的 文字;也有可能涉及印度系文字這樣涉及復(fù)雜布局規(guī)則的文字;另外,還可能有網(wǎng)頁內(nèi)自己指定的文本語言,比如 <span lang=”jp”>日本語</span> 這樣的標(biāo)記,使得日文漢字可以使用日文字體顯示 (因為 Han Unification 導(dǎo)致這些漢字和中文里的漢字使用同樣的代碼點,盡管很多寫法不同),”lang” 屬性也可以在 HTTP 頭、<meta> 或者 <html> 出現(xiàn),用于標(biāo)記整個文檔的全局語言,通常這是一種好的習(xí)慣,方便瀏覽器進(jìn)行字體匹配。
為了統(tǒng)一處理所有這些復(fù)雜的情況,我們要將文本分為由不同語言組成的小段,在有的文本布局引擎里,這個步驟稱為“itemize”,分解后的文本段 常被稱作“text run”,但是具體劃分的規(guī)則可能根據(jù)不同的引擎有所區(qū)別,比如 HarfBuzz 和 ICU 一般是根據(jù)要使用的不同排版類來劃分 (常稱作“shaper”),比如英語和法語可能使用同一個 shaper 排版,那么相鄰的英語和法語文本就會劃分到同一個 run 里,而希伯來文需要另一個 shaper,就劃分到它自己的 run 里,以 HarfBuzz 為例,它有這樣一些 shaper:
- 通用的 (適用于中文、英文等等大多數(shù)布局規(guī)則簡單的語言)
- 阿拉伯文
- 希伯來文
- 印度系文字
- 高棉文
- 緬文
- 諺文
不少瀏覽器還會在這個劃分下面,在確定具體使用的字體之后,根據(jù)使用字體的不同劃分更細(xì)的 run,這種 run 可能稱作“SimpleTextRun”,每個都會使用和相鄰不同的字體,最后把它們逐一交給 shaper 進(jìn)行排版得到要繪制的字形,這樣一來,shaper 的工作就被簡化為在確定的語言、確定的字體下排版確定的文本,生成對應(yīng)的字形和它們應(yīng)該放置的位置、占用的空間。下面先詳細(xì)說說確定字體的步驟。
說到字體,首先必須提到的就是 CSS 里的 font 和 font-family 等規(guī)則。比如這樣的規(guī)則:
- p { font-family: Helvetica, Arial, sans-serif; }
- strong { font-weight: bold; }
如果對于這樣一段文本:
- <p>A quick brown fox <strong>jumps</strong> over the lazy dog.</p>
表示這個段落里優(yōu)先使用 Helvetica 這個 family 的字體,如果找不到,就找 Arial,如果還是找不到,就用瀏覽器設(shè)置的默認(rèn)非襯線字體 (有的瀏覽器,比如 Safari 只給你一個設(shè)置,有的像 Firefox 則允許根據(jù)不同語言設(shè)置,這時可以根據(jù)前面分析得到的文本 run 語言信息來判斷該用哪個),這個過程非常簡單,大家都很好理解。稍微復(fù)雜一點的是“jumps”,它應(yīng)該繼承父元素的 font-family,也用 Helvetica,但不用默認(rèn)的 Regular,而用 Bold 版本,假如找不到 Helvetica Bold,就找 Arial Bold,否則就找瀏覽器設(shè)置的那個字體的 Bold 版本,假如都沒有呢?就要考慮用人工偽造的方式來顯示粗體了,這個且按下不談,先看對于中文常見的情況:CSS 指定的字體沒有覆蓋我們需要的文本時,該怎么做。比如還是上面的 CSS 規(guī)則,但對這樣的文本:
- <p>一只敏捷的狐貍…</p>
這里的“一只敏捷的狐貍”該用什么字體呢?假設(shè) CSS 里具體指定了中文字體,比如 Helvetica, STHeiti, sans-serif,那很簡單,按照英文字體一樣的規(guī)則來判斷:逐個字符嘗試當(dāng)前的字體是否提供了針對該字符的字形,如果沒有則嘗試下一個,要是到了最 后都沒找到匹配的字體呢?CSS 規(guī)范里只簡單的說執(zhí)行“system font fallback”,但這個過程在不同的瀏覽器下可能很不一樣,比如 WebKit 會使用 font-family 列表里的第一個字體和這段文本所屬的語言來尋找 fallback 字體,像 Times 這樣的 serif 字體對應(yīng)的中文 fallback 字體,在 Mac OS X 下是華文宋體 (STSong);而 Firefox 則會根據(jù) sans-serif 這樣的通用 font family 和對應(yīng)的語言匹配到設(shè)置中針對對應(yīng)語言的默認(rèn)字體,比如在 Mac OS X 默認(rèn)的中文非襯線字體是華文黑體 (STHeiti)。Linux 下一般通過 fontconfig 去根據(jù)語言、風(fēng)格等參數(shù)來選擇 fallback,但不同瀏覽器的實現(xiàn)還可能有區(qū)別;Windows 下則一般會使用系統(tǒng)的 Font Linking 機(jī)制,根據(jù)注冊表內(nèi)的 FontSubstitutes 信息來尋找。因為在這里不同的瀏覽器可能有不同的行為,所以建議在 CSS 中寫明對應(yīng)平臺該用的字體。
具體的字體選擇還有一些不太容易注意的細(xì)節(jié),也是各個瀏覽器差異比較大的一點,可能會出現(xiàn)這樣一些問題:
是否支持用字體的 PostScript name 選擇:如 STHeiti 的 Light 版本又稱作 STXihei,或者是否能用 full name 選擇:有的瀏覽器不能正確地將 CSS 里對字體的 font-weight 或者 font-style 等要求映射到特定的字體上,尤其是在字體使用了非標(biāo)準(zhǔn)的 style 命名的情況下,考慮到很多廠商有自己的字體命名規(guī)則,這其實很容易出現(xiàn),像 Helvetica Neue 的 UltraLight, Light, Regular, Medium, Bold 這些不同的 weight,是怎么對應(yīng)到 CSS font-weight 的 100 到 900 數(shù)值上的?這就是特別容易出現(xiàn) bug 的地方。
是否支持按 localized name 選擇:比如能不能用 “宋體” 來代表 “SimSun”。以 Mac OS X 下的瀏覽器為例,F(xiàn)irefox 支持這樣的寫法,但基于 WebKit 的瀏覽器一般不支持,這樣的問題 CSS 規(guī)范沒有限定,所以無論哪種情況都是允許的。
總的說來,如果要保證最大限度的兼容性,在 CSS 書寫的時候應(yīng)該盡可能選擇明確、不容易出錯的寫法,盡量少隱式地讓瀏覽器自己確定 (be explict instead of implict),雖然隱式寫法通常比較簡潔,但除非你 100% 確定想支持的瀏覽器在你想支持的平臺下都能支持這個寫法,否則還是不應(yīng)該輕易用。
CSS3 新增的 @font-face 規(guī)則則是對于現(xiàn)有規(guī)則的擴(kuò)展,提供了 web fonts 功能,但字體匹配算法的邏輯并沒有改變,詳細(xì)的算法可以看 CSS 規(guī)范里的說明。
當(dāng)確定了字體以后,就可以將文本、字體等等參數(shù)一起交給具體的排版引擎,生成字形和位置,然后根據(jù)不同的平臺調(diào)用不同的字體 rasterizer 將字形轉(zhuǎn)換成最后顯示在屏幕上的圖案,一般瀏覽器都會選擇平臺原生的 rasterizer,比如 Mac OS X 下用 Core Graphics,Linux/X11 下用 FreeType,Windows 下用 GDI/DirectWrite 等等。關(guān)于這個步驟,typekit 的這篇 blog 可以作為參考。各個瀏覽器的差異主要來自使用的排版引擎可能對不同的語言支持有差異,調(diào)用 rasterizer 使用的參數(shù)可能有差異 (比如是否啟用 subpixel rendering、使用的 hinting 級別等等),但在同一個操作系統(tǒng)下的效果差別不會很大。
基于以上的介紹,可以嘗試提出一個在現(xiàn)有瀏覽器下,針對中文用戶的,書寫 CSS 字體選擇規(guī)則的建議,如下: |
- 首先確定要選擇字體的元素應(yīng)該使用的字體風(fēng)格,比如是襯線字體、非襯線字體還是 cursive、fantasy 之類的;
- 確定了風(fēng)格之后,先選擇西文字體,優(yōu)先把平臺獨特的、在該平臺下效果更好的字體寫上,比如 Mac OS X 下有 Helvetica 也有 Arial,但 Helvetica (可能) 效果更好,Windows 下則一般只有 Arial,那么寫 Helvetica, Arial 就比 Arial, Helvetica 或者只有 Arial 更好;
- 然后列出中文字體,原則相同,多個平臺共有的字體應(yīng)該盡量放在后邊,獨有的字體放在前面,還需要照顧到 Mac OS X/Linux 下一般用戶習(xí)慣用(細(xì))黑體作為默認(rèn)字體,Windows 下習(xí)慣以宋體作為默認(rèn)字體的情況,比如 STXihei, SimSun 這樣的寫法比較常見,如果寫作 SimSun, STXihei,但 Mac OS X 上裝了 SimSun 效果就不會太好看。
- 最后還是應(yīng)該放上對應(yīng)的 generic family,比如 sans-serif 或者 serif。
- 盡量用字體的基本名稱 (比如 English locale 下顯示的),而不要用本地化過的名稱。除非特殊情況 (Windows 下“某些”瀏覽器在特定編碼下只能支持本地化的字體名稱)。Mac OS X 下字體名稱可以用 Font Book 查到 (菜單 Preview -> Show Font Info),Windows 下字體信息在微軟的網(wǎng)站可以得到,Linux/X11 下可以使用 fc-list 命令查到。
- 字體名稱中包含空格時記住用引號擴(kuò)起,比如 “American Typewritter” 和 “Myriad Pro”。
- 文檔開頭最好指明語言,比如 <html lang=”zh-CN”>,可以使用的語言標(biāo)記參見 W3C 的說明。