在macOS平台上使用Core Text做文本渲染时,很多排版细节并不是由绘制代码直接决定的,而是由字体内部的OpenType特性(OpenType Features)驱动,连字(Ligature)就是其中最典型的一类。所谓连字,是指字体将两个或多个相邻字符组合成一个单一字形的过程,例如英文衬线字体中常见的fi、fl组合会被替换为专门的连字字形。Core Text提供了完善的属性体系来控制这一行为,开发者可以精确选择启用标准连字、历史连字还是上下文交替连字,也可以在特定场景下完全禁用连字。这篇文章将围绕CTLigature相关的字体属性和特性标签展开,帮助你在自己的应用中做出正确的排版决策。

三种连字类型的区别与底层原理
OpenType规范中将连字分为标准连字(common ligature)、自由决定连字(discretionary ligature)、历史连字(historical ligature)以及上下文连字(contextual alternates)等多个类别。标准连字是指那些在现代排版中几乎默认开启的组合,比如fi、ff、ffi、fl等,它们的目的是消除相邻字符笔画之间的碰撞,提升可读性。自由决定连字则属于装饰性质,比如ct、st组合的连字字形,通常只在特定设计风格中使用。历史连字是古代印刷传统保留下来的字形组合,例如长s(ſ)与t的组合,现代排版中一般不启用。上下文连字严格来说属于上下文交替特性,它会根据前后字符的环境自动选择更协调的字形变体。
在Core Text的层面,这些连字行为最终都映射为字体特性标签。常见的标签包括liga(标准连字)、dlig(自由决定连字)、hlig(历史连字)和calt(上下文交替)。每个特性都可以设置开启(值为1)或关闭(值为0)。Core Text在默认情况下会启用liga和calt,这解释了为什么不做任何配置时,某些字体中的fi组合看起来已经变成了单字形。理解这一点很重要:连字的触发条件是字体本身包含对应字形并且特性被开启,两者缺一不可。如果字体没有提供连字字形,即使你开启了hlig特性也不会有任何视觉变化。
另外需要区分的是Core Text中较早的kCTFontLigaturesAttribute属性。这个属性接受一个CFNumberRef,取值0表示禁用所有连字,1表示启用标准连字,2表示启用所有连字(包含自由决定连字和历史连字)。它本质上是对上述OpenType特性标签的高层封装,简单场景下用起来更方便,但灵活性不如直接操作特性标签。如果你需要精细控制某一种连字类型,推荐直接使用kCTFontFeatureSettingsAttribute。
用Core Text代码开启与禁用连字
直接操作OpenType特性标签的方式最为灵活。核心思路是构造一个特性设置字典数组,将其作为kCTFontFeatureSettingsAttribute的值传入字体属性字典,再用这个字典创建字体描述符(CTFontDescriptorRef)并生成最终的CTFont。下面的代码演示了如何禁用标准连字同时启用历史连字:
#import <CoreText/CoreText.h>
CTFontRef CreateFontWithLigatureSettings(CTFontRef baseFont) {
// 构造特性设置数组:每个元素是一个字典
// tag 对应 OpenType 特性标签,value 为 1(开启)或 0(关闭)
CFStringRef tags[] = { CFSTR("liga"), CFSTR("hlig"), CFSTR("calt") };
int values[] = { 0, 1, 0 }; // 关闭标准连字,开启历史连字,关闭上下文交替
CFMutableArrayRef settings = CFArrayCreateMutable(
kCFAllocatorDefault, 3, &kCFTypeArrayCallBacks);
for (int i = 0; i < 3; i++) {
CFTypeRef keys[] = { kCTFontOpenTypeFeatureTag,
kCTFontOpenTypeFeatureValue };
CFTypeRef vals[] = { tags[i], CFNumberCreate(kCFAllocatorDefault,
kCFNumberIntType, &values[i]) };
CFDictionaryRef dict = CFDictionaryCreate(kCFAllocatorDefault,
keys, vals, 2, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFArrayAppendValue(settings, dict);
CFRelease(dict);
CFRelease(vals[1]);
}
CFTypeRef attrs[] = { kCTFontFeatureSettingsAttribute };
CFTypeRef attrVals[] = { settings };
CFDictionaryRef attrDict = CFDictionaryCreate(kCFAllocatorDefault,
attrs, attrVals, 1, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CTFontDescriptorRef desc = CTFontDescriptorCreateWithAttributes(attrDict);
CTFontRef result = CTFontCreateCopyWithAttributes(baseFont, 0.0, NULL, desc);
CFRelease(attrDict);
CFRelease(settings);
CFRelease(desc);
return result;
}这段代码的关键在于kCTFontOpenTypeFeatureTagAttribute和kCTFontOpenTypeFeatureValueAttribute这对键。前者是四字符的特性标签(CFString类型),后者是开启或关闭的整数值。注意这些常量名在较新的SDK中写法可能略有差异,早期版本使用kCTFontOpenTypeFeatureTagAttribute,后来统一为kCTFontOpenTypeFeatureTagAttribute与kCTFontOpenTypeFeatureValueAttribute的组合,实际开发时以当前SDK的头文件为准。
如果只需要简单的开关,使用kCTFontLigaturesAttribute会简洁很多。把它加入字体属性字典并设置对应数值即可。不过在swift项目中,更常见的做法是通过UIFont的fontDescriptor以及UIFontDescriptorFeatureSetting来配置,其底层的feature identifiers(如kLigaturesType和kCommonLigaturesOnSelector)最终仍会翻译成上述OpenType特性。值得一提的是,禁用连字在某些场景是必须的:比如代码编辑器中,用户需要精确看到每一个字符;再比如做文本测量时,连字会导致字符串索引与字形索引不再一一对应,处理不当就会出现光标定位错乱的问题。
连字对文本测量与光标定位的影响
连字带来的不仅是视觉变化,还有布局层面的复杂性。当多个字符被合并为一个字形后,CTLine内部的CTRun会把这些字符归入同一个字形集群,字符到字形的映射关系从一对一变为一对多。如果你在实现自定义文本编辑组件时直接用CTRunGetGlyphs取出的字形数量去推算字符数量,结果就会出错。正确的做法是使用CTRunGetStringIndices获取每个字形对应的字符串索引范围,或者用CTLineGetOffsetForStringIndex和CTLineGetStringIndexForPosition在字符串索引与像素位置之间换算,Core Text会自动处理连字造成的偏移。
另一个容易踩的坑是命中测试。带连字的文本中,光标停在fi连字内部时,视觉上应该显示在连字前还是连字后,不同平台有不同策略。macOS的系统行为是将光标放置在整个连字集群的边界处,即索引要么指向连字的第一个字符,要么指向最后一个字符之后。开发者如果自己绘制光标,需要用CTRunGetStringRange结合CTRunGetTypographicBounds来判断当前索引是否落在连字集群内部,并做相应的吸附处理。否则用户会感觉光标“卡住”或者位置跳动。
在排版性能方面,开启更多连字类型意味着字形替换阶段的工作量增加,但对于现代硬件来说这部分开销几乎可以忽略。真正需要注意的是,如果你在每一帧都重新创建带特性设置的CTFont,CFDictionary和CTFontDescriptor的构造会带来不必要的开销。合理的做法是缓存配置好的CTFont实例,只在字体或特性需求变化时重建。此外,某些字体的hlig和dlig字形质量参差不齐,启用前最好实际渲染验证一下效果,避免出现字形基线对不齐或间距异常的问题。
不同应用场景下的连字策略建议
针对不同类型的应用,连字策略应当有所区分。对于阅读类应用(电子书、新闻客户端),建议保持默认行为,即启用标准连字和上下文交替,这符合用户对印刷品质的预期;如果应用主打古典文学或学术出版内容,可以有条件地启用历史连字,并在设置中提供开关让用户自主选择。对于代码编辑器和终端模拟器,建议默认禁用所有连字,因为程序员需要精确辨认同形字符,比如某些编程字体中的连字会把<=和!=渲染成箭头或不等号样式,虽然看起来美观,但在调试和教学场景中容易引起误解。当然也可以像一些流行的等宽字体编辑器那样,把连字作为可配置项暴露给用户。
对于富文本编辑器,情况更复杂一些。NSAttributedString本身支持通过kCTFontLigaturesAttribute或字体特性控制连字,你可以针对不同文本范围设置不同的连字策略,比如正文启用标准连字而代码块禁用。还要注意,将文本导出为PDF或跨平台分享时,目标渲染引擎对连字的支持程度可能不同,最稳妥的做法是在导出环节统一处理字形,或者明确保留原始文本让接收方自行排版。
最后总结一下核心要点:Core Text中的连字控制有两条路径,简洁的kCTFontLigaturesAttribute适合整体开关,精细的kCTFontFeatureSettingsAttribute配合liga、dlig、hlig、calt标签适合逐类控制;连字启用与否不仅影响外观,还会改变字符与字形的映射关系,任何涉及文本测量、光标定位和命中测试的代码都必须正确处理字形集群;实际产品中应根据应用类型制定差异化的连字默认值,并把选择权适度交给用户。掌握这些细节后,你就能在macOS应用中实现既美观又精准的文本排版。
Core Text连字处理CTLigature修改时间:2026-09-05 15:35:13