浏览器如何渲染文本

浏览器是我们最常用的软件之一,文本又是网页中最主要的元素,在浏览器显示文本的过程中有许多有趣的细节,值得展开来讲讲,或许能减少一些误解。这是一个比较粗略的,概括性的介绍,尽可能不涉及过多的技术细节和具体实现,而立足于给 Web 开发者和设计师提供一些正确的概念。

下面的介绍主要根据我对 WebKit 和 Gecko (Firefox) 的印象来谈,其他的浏览器也大致相同,如有阙漏之处欢迎指出。

解码

当浏览器收到来自 Web 服务器的网页数据之后,第一步是要把它解码成可以阅读的文本,因为历史原因,不同区域和语言的网页可能会使用不同的编码方式,而浏览器判断编码主要是依据以下方法:

  1. Web 服务器返回的 HTTP 头中的 Content-Type: text/html; charset= 信息,这一般有最高的优先级;
  2. 网页本身 meta header 中的 Content-Type 信息的 charset 部分,对于 HTTP 头未指定编码或者本地文件,一般是这么判断;
  3. 假如前两条都没有找到,浏览器菜单里一般允许用户强制指定编码;
  4. 部分浏览器 (比如 Firefox) 可以选择编码自动检测功能,使用基于统计的方法判断未定编码。

分段

编码确定后,网页就被解码成了 Unicode 字符流,可以进行进一步的处理,比如 HTML 解析了,不过我们这里跳过 HTML/XML 解析的细节,单讲得到了解析后的文本元素之后该怎么处理。

因为我们得到的文本可能是很多种语言混杂的,里面可能有中文、有英文,它们可能要用不同的字体显示;也可能有阿拉伯文、希伯来文这种从右到左书写的文字;也有可能涉及印度系文字这样涉及复杂布局规则的文字;另外,还可能有网页内自己指定的文本语言,比如 <span lang="jp">日本语</span> 这样的标记,使得日文汉字可以使用日文字体显示 (因为 Han Unification 导致这些汉字和中文里的汉字使用同样的代码点,尽管很多写法不同),"lang" 属性也可以在 HTTP 头、<meta> 或者 <html> 出现,用于标记整个文档的全局语言,通常这是一种好的习惯,方便浏览器进行字体匹配。

为了统一处理所有这些复杂的情况,我们要将文本分为由不同语言组成的小段,在有的文本布局引擎里,这个步骤称为“itemize”,分解后的文本段常被称作“text run”,但是具体划分的规则可能根据不同的引擎有所区别,比如 HarfBuzzICU 一般是根据要使用的不同排版类来划分 (常称作“shaper”),比如英语和法语可能使用同一个 shaper 排版,那么相邻的英语和法语文本就会划分到同一个 run 里,而希伯来文需要另一个 shaper,就划分到它自己的 run 里,以 HarfBuzz 为例,它有这样一些 shaper:

不少浏览器还会在这个划分下面,在确定具体使用的字体之后,根据使用字体的不同划分更细的 run,这种 run 可能称作“SimpleTextRun”,每个都会使用和相邻不同的字体,最后把它们逐一交给 shaper 进行排版得到要绘制的字形,这样一来,shaper 的工作就被简化为在确定的语言、确定的字体下排版确定的文本,生成对应的字形和它们应该放置的位置、占用的空间。下面先详细说说确定字体的步骤。

字体

说到字体,首先必须提到的就是 CSS 里的 fontfont-family 等规则。比如这样的规则:

p { font-family: Helvetica, Arial, sans-serif; }
strong { font-weight: bold; }

如果对于这样一段文本:

<p>A quick brown fox <strong>jumps</strong> over the lazy dog.</p>

表示这个段落里优先使用 Helvetica 这个 family 的字体,如果找不到,就找 Arial,如果还是找不到,就用浏览器设置的默认非衬线字体 (有的浏览器,比如 Safari 只给你一个设置,有的像 Firefox 则允许根据不同语言设置,这时可以根据前面分析得到的文本 run 语言信息来判断该用哪个),这个过程非常简单,大家都很好理解。稍微复杂一点的是“jumps”,它应该继承父元素的 font-family,也用 Helvetica,但不用默认的 Regular,而用 Bold 版本,假如找不到 Helvetica Bold,就找 Arial Bold,否则就找浏览器设置的那个字体的 Bold 版本,假如都没有呢?就要考虑用人工伪造的方式来显示粗体了,这个且按下不谈,先看对于中文常见的情况:CSS 指定的字体没有覆盖我们需要的文本时,该怎么做。比如还是上面的 CSS 规则,但对这样的文本:

<p>一只敏捷的狐狸...</p>

这里的“一只敏捷的狐狸”该用什么字体呢?假设 CSS 里具体指定了中文字体,比如 Helvetica, STHeiti, sans-serif,那很简单,按照英文字体一样的规则来判断:逐个字符尝试当前的字体是否提供了针对该字符的字形,如果没有则尝试下一个,要是到了最后都没找到匹配的字体呢?CSS 规范里只简单的说执行“system font fallback”,但这个过程在不同的浏览器下可能很不一样,比如 WebKit 会使用 font-family 列表里的第一个字体和这段文本所属的语言来寻找 fallback 字体,像 Times 这样的 serif 字体对应的中文 fallback 字体,在 Mac OS X 下是华文宋体 (STSong);而 Firefox 则会根据 sans-serif 这样的通用 font family 和对应的语言匹配到设置中针对对应语言的默认字体,比如在 Mac OS X 默认的中文非衬线字体是华文黑体 (STHeiti)。Linux 下一般通过 fontconfig 去根据语言、风格等参数来选择 fallback,但不同浏览器的实现还可能有区别;Windows 下则一般会使用系统的 Font Linking 机制,根据注册表内的 FontSubstitutes 信息来寻找。因为在这里不同的浏览器可能有不同的行为,所以建议在 CSS 中写明对应平台该用的字体

具体的字体选择还有一些不太容易注意的细节,也是各个浏览器差异比较大的一点,可能会出现这样一些问题:

  • 是否支持用字体的 PostScript name 选择:如 STHeiti 的 Light 版本又称作 STXihei,或者是否能用 full name 选择:有的浏览器不能正确地将 CSS 里对字体的 font-weight 或者 font-style 等要求映射到特定的字体上,尤其是在字体使用了非标准的 style 命名的情况下,考虑到很多厂商有自己的字体命名规则,这其实很容易出现,像 Helvetica Neue 的 UltraLight, Light, Regular, Medium, Bold 这些不同的 weight,是怎么对应到 CSS font-weight 的 100 到 900 数值上的?这就是特别容易出现 bug 的地方。
  • 是否支持按 localized name 选择:比如能不能用 "宋体" 来代表 "SimSun"。以 Mac OS X 下的浏览器为例,Firefox 支持这样的写法,但基于 WebKit 的浏览器一般不支持,这样的问题 CSS 规范没有限定,所以无论哪种情况都是允许的。

总的说来,如果要保证最大限度的兼容性,在 CSS 书写的时候应该尽可能选择明确、不容易出错的写法,尽量少隐式地让浏览器自己确定 (be explict instead of implict),虽然隐式写法通常比较简洁,但除非你 100% 确定想支持的浏览器在你想支持的平台下都能支持这个写法,否则还是不应该轻易用。

CSS3 新增的 @font-face 规则则是对于现有规则的扩展,提供了 web fonts 功能,但字体匹配算法的逻辑并没有改变,详细的算法可以看 CSS 规范里的说明

渲染

当确定了字体以后,就可以将文本、字体等等参数一起交给具体的排版引擎,生成字形和位置,然后根据不同的平台调用不同的字体 rasterizer 将字形转换成最后显示在屏幕上的图案,一般浏览器都会选择平台原生的 rasterizer,比如 Mac OS X 下用 Core Graphics,Linux/X11 下用 FreeType,Windows 下用 GDI/DirectWrite 等等。关于这个步骤,typekit 的这篇 blog 可以作为参考。各个浏览器的差异主要来自使用的排版引擎可能对不同的语言支持有差异,调用 rasterizer 使用的参数可能有差异 (比如是否启用 subpixel rendering、使用的 hinting 级别等等),但在同一个操作系统下的效果差别不会很大。

建议

基于以上的介绍,可以尝试提出一个在现有浏览器下,针对中文用户的,书写 CSS 字体选择规则的建议,如下:

  1. 首先确定要选择字体的元素应该使用的字体风格,比如是衬线字体、非衬线字体还是 cursive、fantasy 之类的;
  2. 确定了风格之后,先选择西文字体,优先把平台独特的、在该平台下效果更好的字体写上,比如 Mac OS X 下有 Helvetica 也有 Arial,但 Helvetica (可能) 效果更好,Windows 下则一般只有 Arial,那么写 Helvetica, Arial 就比 Arial, Helvetica 或者只有 Arial 更好;
  3. 然后列出中文字体,原则相同,多个平台共有的字体应该尽量放在后边,独有的字体放在前面,还需要照顾到 Mac OS X/Linux 下一般用户习惯用(细)黑体作为默认字体,Windows 下习惯以宋体作为默认字体的情况,比如 STXihei, SimSun 这样的写法比较常见,如果写作 SimSun, STXihei,但 Mac OS X 上装了 SimSun 效果就不会太好看。
  4. 最后还是应该放上对应的 generic family,比如 sans-serif 或者 serif
  5. 尽量用字体的基本名称 (比如 English locale 下显示的),而不要用本地化过的名称。除非特殊情况 (Windows 下“某些”浏览器在特定编码下只能支持本地化的字体名称)。Mac OS X 下字体名称可以用 Font Book 查到 (菜单 Preview -> Show Font Info),Windows 下字体信息在微软的网站可以得到,Linux/X11 下可以使用 fc-list 命令查到。
  6. 字体名称中包含空格时记住用引号扩起,比如 "American Typewritter""Myriad Pro"
  7. 文档开头最好指明语言,比如 <html lang="zh-CN">,可以使用的语言标记参见 <a href="http://www.w3 pharmacie du viagra.org/International/articles/language-tags/”>W3C 的说明。

网页 CJK 竖排的最新进展

上个月关于 CSS 的一个重要的新闻是 WebKit 开始支持 CJK 竖排 (通过 -webkit-writing-mode),而且时隔七年之后 (上一个版本还是 2003 年制定的),W3C 的 CSS 工作组也发布了新的 CSS Writing Mode Level 3 工作草案,和以前的版本有较大的区别,WebKit 现在就是按照这个草案实现的竖排支持,虽然它的实现还不完整。下面简单介绍一下新草案的变化。

在原有草案中,writing-mode 属性是 directionblock-flow 属性的合成,你可以交叉地组合这两个属性,构成 writing-mode:比如 lr-tb 表示文字从左到右,块排列从上到下。而在新草案里,block-flow 不再是一个独立的概念,被并入了 writing-mode 中,新的 writing-mode 有以下几种选择:

  • horizontal-tb: 默认情况,从上到下,从左到右的横排书写形式。

    horizontal-tb
    horizontal-tb

  • vertical-rl: 块按从右到左排列,文字则从上到下,这是典型的直排情况。

    vertical-rl
    vertical-rl

  • vertical-lr: 虽然是竖排,但块则从左到右排列。这主要用于内蒙古使用的蒙古语满语

    vertical-lr
    vertical-lr

还可以注意到经过讨论,非常少用的从下往上横排书写形式 (horizontal-bt) 被去除了。

保留了 direction 属性,但它和 CSS2.1 中的效果一样,仍然用于控制文本在行内是从左到右还是从右到左书写,以及 Unicode BIDI 双向混合文字的情况。

同时,新增了一个 text-orientation 属性用于控制行内字符的旋转,例如在典型的中文竖排文稿中,拉丁字符应该顺时针旋转 90 度。目前的草案规定了以下这些模式:

  • vertical-right: 默认的情况,将非文本语言的字符 (比如汉字中间嵌入的拉丁字符) 顺时针旋转 90 度,其他字符不变。
  • upright: 不旋转上述字符,保持和其他字符一样的方向。(这是目前 WebKit 实现的做法,等于什么都没处理)
  • rotate-right: 把所有字符都顺时针旋转 90 度。
  • rotate-left: 把所有字符都逆时针旋转 90 度。
  • rotate-normal: 在 vertical-rl 书写模式中等于 rotate-right,在 vertical-lr 中等于 rotate-left
  • auto: 除了对 SVG1 的 glyph orientation mode 支持以外,其他情况下等于 vertical-right

下图中左侧是 vertical-right/auto 的效果,右侧是 upright 的效果:

最后,新增了 text-combine 属性用于控制在竖排时同一行内要塞进多个非 CJK 字符的情况,在日文排版中称为“縦中横”。它有两个选择:

  • none: 不做特殊处理。
  • horizontal: 在竖排情况下,首先浏览器应该尝试用对应字体中提供的专门的合并后的字形替代,如果没有,则浏览器可以尝试缩小这些字符以适应宽度,或者放弃合并。效果如图:
    縦中横
    縦中横

不过这个属性目前还很不成熟,仅仅初步把概念规范化了,短期内估计不会有浏览器尝试实现。此外这份草案还没考虑行内割注 (日文中称为 warichu) 的情况:

割注
割注

目前在 Mac OS X 下 WebKit 已经完整实现了这一版草案的 writing-mode,大部分的 bug 也都已经扫除 (其他平台的实现情况参见 Koan-Sin Tan 的说明),但是还没开始实现 text-orientation,然而要保证可用的竖排效果,text-orientation 的支持是不可或缺的。

State of The Art Web Typography

We have reached the state that Web typography can be much better than it used to be. What kind of tools is in our hands?

  • Hyphenator, with this JavaScript hyphenation support, reading justified text on your browser will no longer be a pain.
  • Knuth and Plass Line Breaking in JavaScript, combining this and Hyphenator then you will get beautiful typesetting like this sample.
  • optimizeLegibility, CSS rule text-rendering: optimizeLegibility; will turn on cross browser kerning-pairs & ligatures, which is also critical for good typography. It does have some drawbacks though.
  • Web fonts solutions like TypeKit and high quality free web fonts from websites like Font Squirrel, provided much more vivid user experience than standard web fonts like Verdana and Georgia, but they should be chosen very carefully, otherwise the results can be much worse.
  • -moz-font-feature-settings, this feature, although it’s Firefox 4 only, can be very fun to experiment with, since it opens an entire new world of rich OpenType typography to the web. Combining this with web fonts solutions can make it even more useful.
  • -webkit-font-smoothing, when used in a clever way, can improve legibility (and some designers prefer grayscale antialiasing to subpixel antialiasing, that’s why it’s so popular nowadays). Unfortunately there are many existing websites abusing this CSS rule, including Apple and Twitter. In general, I suggest leaving it alone unless it’s absolutely necessary.
  • SmartyPants will convert ugly quotes, dashes and consecutive dots into beautiful punctuations that you really meant to use. It saves a lot of editing efforts.
  • MathJax, the only weakness of web typography comparing to TeX is math equations, MathJax is so good that it totally eliminated this weakness.

Think about it: what if we create an automatic web typesetting tool, combining all these efforts, how beautiful the result we will get? That’s what I am trying to do recently.

Web 字体的出路在哪里?

浏览器直接可用的 Web 字体在去年被重新提起,可是没过一段时间就沉寂下去,最近一期 A List Apart 上 Jeffrey Zeldman 采访 David Berlow 的这篇 Real Fonts on the Web 又引起了新一轮的讨论,这里摘录一些有趣的话。

David Berlow:

How important dynamically rendered type is to design and use on the web must now be clear. In addition, the only other option—that the type industry cede its intellectual property to the public without permission—is not going to happen.

Tal Leming:

There should be a new file extension for this. I propose “.wtf” – “WebType Font”.

Mark Pilgrim:

… What he fails to mention is that every font-consuming application on every platform on every computer on Earth will need to be “upgraded” to “respect” this permissions table. Because otherwise they’re not really permissions, are they? They’re just useless bits taking valuable chunks out of my metered bandwidth plan. Like the bozo bit without the bozo.

Jonathan Hoefler:

All of the type designers I know desperately want to find a way to enable people to use fonts online, and not just because we’re capitalist stooges, but because we live our lives online.

Bruce Schneier:

Truth be told, I don’t know. I feel rather like the physicist who just explained relativity to a group of would-be interstellar travelers, only to be asked: ‘How do you expect us to get to the stars, then?’ I’m sorry, but I don’t know that, either.

总结起来,未来还是很黑暗。