iOS 上利用 fallback 机制为不同语言的文字 (script) 设定字体,从而使得文本混排更为优雅

4,524 阅读4分钟

如果您是一位 WEB 开发者,相信您对 CSS 的 font-family 属性一定不会陌生。通常我们会为 font-family 属性设置一长串的字体(家族)列表,就像这样的:

.text { font-family: Menlo, Monaco, Consolas, "Courier New", monospace, Arial, "Microsoft YaHei", "黑体", "宋体", sans-serif; }

有人肯定会问,为什么要这么设置啊?如果你足够细心,你一定会发现当你浏览该网页时英文字体和中文字体并不是同一种字体,如果你在不同的操作系统甚至不同的电脑上看到网页所呈现的字体也不一样。这又是为什么呢?

此外,在您使用 Microsoft Word 进行段落排版的时候,你会发现 Word 可以自动对中文样式应用中文字体,对英文样式应用英文字体。这又是如何做到的呢?

在实际的排版需求中,为了好看,我们通常会需要针对不同语言的文字 (script) 进行不同的字体设定,以达到最佳的视觉效果。当然,对于我们来说,最常见的还是中英文混排。通常设计人员给出的设计稿非常漂亮,可是中英文使用了不同的字体,我们开发人员该如何高保真地还原设计稿的原始设计呢?

很显然,对于 WEB 开发者来说,已经有了很好的解决方案,而对于其他客户端的同学来说,这估计应该就有点犯难了,我们通常会告诉设计人员:“系统不支持” 抑或 “做不了” 之类的话。那事实上,到底能不能做到呢?答案是肯定的。

要实现这个,我们先来了解一个概念:fallback 机制。

这种机制通常是首先规定拉丁字母、西里尔字母、希腊字母等西文的字体,然后以一定的顺序分别规定阿拉伯字母、天城文(例如印度文)、日文、韩文、简体中文、繁体中文等文字的字体。文本引擎会尽量用优先的字体来显示当前遇到的字符,如果 fallback 列表中最靠前的字体不支持此字符,就向后找(这就是所谓 fallback)。CSS 的 font-family 属性基本也是这样工作的。

Windows 其实也是类似这样的。目前 Windows 7 的西文字体是 Segoe UI,简体中文字体是微软雅黑,繁体中文字体是微软正黑,日文字体是 Meiryo,天城文字体是 Mangal…… 所以你根本不需要这样的软件,你的操作系统已经是这样了。

因为中日韩汉字共享 Unicode code point,所以当一个字(比如「直」这个字)同时受到简体中文、繁体中文、日文、韩文字体的支持,操作系统会根据当前的 locale 以及系统语言列表的顺序选择最优先的书写系统的字体。

然而,很少谈及移动操作系统上的字体 fallback 机制,更别说实现了,还好我经过不断查找,找到一些蛛丝马迹。由于本人是一名 iOS 开发者,所以我们以 iOS 为例,来看看它的实现方式。

通常,我们使用如下的方式创建一个字体:

let systemFont = UIFont.systemFont(ofSize: fontSize)

显然,如果您还这样创建,肯定无法实现我们的目标。那应该怎么做呢?我们需要使用到 UIFontDescriptor,代码如下:

public extension UIFont {
    convenience init(names: [String], size: CGFloat) {
        
        if names.first != nil {
            let mainFontName = names.first!
            
            let descriptors = names.map { UIFontDescriptor(fontAttributes: [.name: $0]) }
            
            let attributes: [UIFontDescriptor.AttributeName: Any] = [
                UIFontDescriptor.AttributeName.cascadeList: descriptors,
                UIFontDescriptor.AttributeName.name: mainFontName,
                UIFontDescriptor.AttributeName.size: size,
                ]
            
            let customFontDescriptor: UIFontDescriptor = UIFontDescriptor(fontAttributes: attributes)
            self.init(descriptor: customFontDescriptor, size: size)
        }
        else{
            let systemFont = UIFont.systemFont(ofSize: size)
            let systemFontDescriptor: UIFontDescriptor = systemFont.fontDescriptor
            self.init(descriptor: systemFontDescriptor, size: size)
        }
    }
    
    convenience init(families: [String], size: CGFloat, weight: UIFont.Weight = .regular) {
        
        if families.first != nil {
            let mainFontFamily = families.first!
            let descriptors = families.map { UIFontDescriptor(fontAttributes: [.family: $0]) }
            let traits = [UIFontDescriptor.TraitKey.weight: weight]
            
            let attributes: [UIFontDescriptor.AttributeName: Any] = [
                UIFontDescriptor.AttributeName.cascadeList: descriptors,
                UIFontDescriptor.AttributeName.family: mainFontFamily,
                UIFontDescriptor.AttributeName.size: size,
                UIFontDescriptor.AttributeName.traits: traits
            ]
            
            let customFontDescriptor: UIFontDescriptor = UIFontDescriptor(fontAttributes: attributes)
            self.init(descriptor: customFontDescriptor, size: size)
        }
        else{
            let systemFont = UIFont.systemFont(ofSize: size, weight: weight)
            let systemFontDescriptor: UIFontDescriptor = systemFont.fontDescriptor
            self.init(descriptor: systemFontDescriptor, size: size)
        }
    }
}

调用示例:

let text = "これは日本語文章と Roman Text の混植文章です。美しいヒラギノと San Francisco で日本語とローマ字を書きます。System Font のフォントメトリクスには独自の調整が入っています。\n\nあのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさをもつ青いそら、\nうつくしい森で飾られたモーリオ市、\n郊外のぎらぎらひかる草の波。\n祇辻飴葛蛸鯖鰯噌庖箸\n底辺直卿蝕薩化\nABCDEFGHIJKLM\nabcdefghijklm\n1234567890iClockᴹᴵᴺᴵClockªMINI"
let font = UIFont(families: ["Lucida Grande", "Baskerville", "Apple SD Gothic Neo"], size: 20, weight: .medium)
//let font = UIFont(names: ["LucidaGrande", "Baskerville", "AppleSDGothicNeo-Thin"], size: 20)
label.font = font
label.text = text

其中 family 和 font name 这两个别搞混了,不知道怎么在 iOS 上看字体的 family 和 font name 的可以下载这个 App:字体预览(itunes.apple.com/cn/app/字体预览…)

如果是自定义字体,也可以在同一个wifi的局域网中将字体上传到 App 里查看。

演示动画:

Github源码: github.com/pcjbird/fbC…

原文链接:blog.lessney.com/blog/2019/1…

参考链接:

是否有一种软件能够对不同语言的文字指定不同的字体?

www.zhihu.com/question/20…

Fallback to pure Japanese font. No more Chinese font in Japanese text on iOS apps.

github.com/usagimaru/F…