导读:本期聚焦于陆星河创作的《macOS Core Text字体度量详解:如何用CTFontMetrics获取字体上升、下降、行高与基线信息?》,敬请观看详情。文本排版时基线位置总是对不齐?行高计算和设计稿不一致?问题的根源往往在字体度量数据。Core Text提供了完善的字体度量API,通过CTFontCopyTraits、CTFontGetAscender、CTFontGetDescender等函数,可以直接读取字体的上升值、下降值、前导值以及真实的基线位置。本文详细讲解ascent、descent、leading这几个核心概念在坐标系中的含义,分析CTFontMetrics相关接口的返回值与unitsPerEm的换算关系,并给出在Swift和Objective-C中计算行高、处理负descent、适配中文字体的完整代码示例,同时对比UIFont与CTFont度量行为的差异,帮助你在做富文本编辑器或自定义排版引擎时精确控制每一行的高度。

做自定义文本渲染的开发者几乎都会遇到同一个问题:同样的字号,不同的字体渲染出来的行高却不一样,基线位置也总是对不上设计稿。这不是bug,而是每个字体文件内部自带一套度量数据(metrics),Core Text在排版时就是依据这套数据来计算行高和基线的。理解并正确读取这些度量值,是掌握macOS文本排版的关键一步。

macOS Core Text字体度量详解:如何用CTFontMetrics获取字体上升、下降、行高与基线信息?

字体度量的核心概念:ascent、descent、leading

在深入API之前,先要把几个容易混淆的概念厘清。字体坐标系以基线(baseline)为原点,向上为正方向(macOS默认的Core Text坐标系是y轴向上的,这点和UIKit的坐标系相反)。

ascent(上升值)指的是从基线到字体最高字形顶部的距离。注意这里说的是字形,不是em框。比如字母f、l或者带声调的é,它们的顶部决定了ascender的位置。descent(下降值)是从基线向下延伸的深度,像字母g、y、p的下伸部分都落在这一区域。在Core Text中,descent返回的是一个正值,表示基线以下的距离(在y轴向上的坐标系里,字形实际的y坐标是负的,但API返回值取了绝对值)。leading(前导)是字体设计者建议的行间距额外空间,行高通常等于ascent加descent加leading。

另一个重要概念是unitsPerEm(每个em对应的字体设计单位数),常见的TrueType字体是2048,OpenType字体是1000。字体文件中存储的原始度量值都是设计单位,需要除以unitsPerEm再乘以字号才能换算成实际的点数。Core Text的API已经帮你做了这层换算,直接返回以点为单位浮点值。

用Core Text API读取字体度量数据

Core Text没有名为CTFontMetrics的直接结构体,字体度量信息都通过CTFont的一系列函数获取。最常用的有CTFontGetAscender、CTFontGetDescender、CTFontGetLeading,以及获取更完整信息的CTFontCopyTraits和CTFontGetVerticalOffsets。

下面是一段Swift代码,演示如何创建CTFont并读取全部度量信息:

import CoreText

let font = CTFontCreateWithName("Helvetica" as CFString, 16.0, nil)

let ascent = CTFontGetAscender(font)      // 上升值
let descent = CTFontGetDescent(font)      // 下降值
let leading = CTFontGetLeading(font)      // 前导值
let unitsPerEm = CTFontGetUnitsPerEm(font) // 设计单位数
let capHeight = CTFontGetCapHeight(font)  // 大写字母高度
let xHeight = CTFontGetXHeight(font)      // 小写字母x的高度

let lineHeight = ascent + descent + leading
print("ascent: \(ascent), descent: \(descent), leading: \(leading)")
print("行高: \(lineHeight)")

以Helvetica 16pt为例,ascent约为14.35,descent约为4.11,leading为0,算出来的行高约为18.46。你可以用Font Book或者跨平台的字体查看工具核对字体文件中的hhea表数据,会发现这些值正是从表里换算出来的。

如果需要更细致的基线信息,比如上标基线、下标基线、斜体字的旁侧偏移,可以用CTFontGetOpticalBoundsForGlyph配合CTFontGetBaselineOffsets类函数。另外,CTFontCreatePathForGlyph返回的字形路径坐标也是以基线为原点的,做文字转矢量图形时必须先理解这一点。

Objective-C环境下的度量获取与实践

老项目大量使用Objective-C,写法上略有区别但API完全一致。同时要注意CFRelease的手动内存管理:

#import <CoreText/CoreText.h>

CTFontRef font = CTFontCreateWithName(CFSTR("PingFangSC-Regular"), 14.0, NULL);
CGFloat ascent = CTFontGetAscender(font);
CGFloat descent = CTFontGetDesender(font); // 注意正确函数名是 CTFontGetDescender
CGFloat descentValue = CTFontGetDescender(font);
CGFloat leading = CTFontGetLeading(font);

// 计算一行文字所需的垂直空间
CGFloat lineHeight = ceil(ascent + descentValue + leading);
NSLog(@"行高: %f", lineHeight);

CFRelease(font);

上面代码特意保留了一个常见笔误示范:CTFontGetDescender容易拼错,正确拼写是Descender而不是Desender,编译器会直接报错,但如果不小心写成了其他相近函数,就会得到意料之外的结果。

实际项目中经常需要计算某段文字的总高度。配合CTFramesetter使用时,更推荐直接用CTFramesetterSuggestFrameSizeWithConstraints,它会自动考虑换行后的多行情况:

NSAttributedString *attrString =
    [[NSAttributedString alloc] initWithString:@"测量这段文字的高度"
        attributes:@{(id)kCTFontAttributeName: (__bridge id)font}];

CTFramesetterRef framesetter =
    CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);

CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(
    framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(300, CGFLOAT_MAX), NULL);

NSLog(@"建议尺寸: %@", NSStringFromCGSize(size));
CFRelease(framesetter);

中文字体与多字体混排的度量陷阱

处理中文内容时,度量问题会集中爆发。中文字形的笔画往往超出拉丁字体的em框,所以中文字体的ascent和descent普遍偏大。以苹方PingFang SC为例,14pt下ascent加descent约19.6,明显大于Helvetica。这就是为什么很多App切换到中文字体后行距突然变松的原因。

富文本混排时,一行内可能有多个字体和字号,Core Text的做法是取这一行中所有字体度量的最大值来决定行高。实现自定义排版引擎时,你需要遍历该行每个CTRun的属性,用CTRunGetTypographicBounds拿到每个run的ascent、descent:

var ascent: CGFloat = 0
var descent: CGFloat = 0
var leading: CGFloat = 0
let width = CTRunGetTypographicBounds(run, CFRange(location: 0, length: 0),
                                       &ascent, &descent, &leading)
// width是该run的排版宽度,行高则取各行中最大的 ascent + descent

还有一个典型陷阱:有些字体的leading是负数或零,有些字体的descent设计值包含大量内部留白。如果产品要求视觉行高固定,不能简单依赖字体自带度量,常见做法是用kCTParagraphStyleSpecifierLineHeightMultiple或者手动设置行间距,把字体度量和视觉需求解耦。

CTFont与UIFont度量行为的差异

在AppKit和UIKit中,UIFont(macOS 10.15后NSFont与UIFont统一)是对CTFont的封装,但两者的度量API存在微妙差异。UIFont的ascender属性和CTFontGetAscender的返回值在个别字体上并不完全相同,因为UIFont可能会做平台的行高补偿处理。

另外要留意CTLineGetTypographicBounds返回的行高度量,与CTFrameDraw绘制时行的origin.y计算是配套的。如果你自己用CTFrameGetLines拿到CTLine数组后逐行绘制,每行的绘制起点是baseline位置,也就是lineOrigin.y加上ascent才是这一行文字视觉上的顶部:

let lines = CTFrameGetLines(frame) as! [CTLine]
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: 0), &origins)

context.saveGState()
context.textPosition = origins[0] // 该行基线位置
CTLineDraw(lines[0], context)
context.restoreGState()

总结一下要点:先通过CTFontGetAscender等函数拿到以点为单位的度量值;绘制时记住返回的是基线原点,不是顶部坐标;混排时取各行度量的最大值;中文场景不要迷信字体自带的行高,必要时用段落样式覆盖。掌握这些之后,无论是做富文本编辑器、表格控件还是自定义排版引擎,行高和基线都不再是玄学问题。

CTFontMetricsCore Text字体度量修改时间:2026-09-14 04:27:46

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。