核心文本中的行间距是如何工作的?(为什么它与NSLayoutManager不同?)

29

我正在尝试使用Core Text函数绘制文本,并希望行间距与使用NSTextView时尽可能接近。

以这个字体为例:

NSFont *font = [NSFont fontWithName:@"Times New Roman" size:96.0];

如果我在NSTextView中使用这种字体,它的行高为111.0。

NSLayoutManager *lm = [[NSLayoutManager alloc] init];
NSLog(@"%f", [lm defaultLineHeightForFont:font]); // this is 111.0

现在,如果我使用Core Text进行相同的操作,结果为110.4(假设您可以通过将上升、下降和行间距相加来计算行高)。

CTFontRef cFont = CTFontCreateWithName(CFSTR("Times New Roman"), 96.0, NULL);
NSLog(@"%f", CTFontGetDescent(cFont) + CTFontGetAscent(cFont) + 
             CTFontGetLeading(cFont)); // this is 110.390625

这个值非常接近111.0,但对于某些字体来说,差异会更大。例如,在Helvetica字体中,NSLayoutManager给出了115.0而CTFont ascent + descent + leading = 96.0。显然,对于Helvetica字体,我将无法使用ascent + descent + leading来计算行间距。

因此,我想使用CTFrame和CTFramesetter来布置几行,并从中获取行高。但那也会给出不同的值。

CTFontRef cFont = CTFontCreateWithName(CFSTR("Times New Roman"), 96.0, NULL);
NSDictionary *attrs = [NSDictionary dictionaryWithObject:(id)cFont forKey:(id)kCTFontAttributeName];
NSAttributedString *threeLines = [[NSAttributedString alloc] initWithString:@"abcdefg\nabcdefg\nabcdefg" attributes:attrs];

CTFramesetterRef threeLineFramesetter =  CTFramesetterCreateWithAttributedString((CFAttributedStringRef)threeLines);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0.0, 0.0, 600.0, 600.0));
CTFrameRef threeLineFrame = CTFramesetterCreateFrame(threeLineFramesetter, CFRangeMake(0, 0), path, NULL);

CGPoint lineOrigins[3];
CTFrameGetLineOrigins(threeLineFrame, CFRangeMake(0, 0), lineOrigins);
NSLog(@"space between line 1 and 2: %f", lineOrigins[0].y - lineOrigins[1].y); // result: 119.278125
NSLog(@"space between line 2 and 3: %f", lineOrigins[1].y - lineOrigins[2].y); // result: 113.625000

因此,行间距现在与我在NSTextView中使用的111.0更加不同,而且不是每一行都相等。似乎换行符会添加一些额外的空间(即使paragraphSpacingBefore的默认值为0.0)。

我现在正在解决这个问题,通过NSLayoutManager获取行高,然后逐个绘制每个CTLine,但我想知道是否有更好的方法来解决这个问题。

3个回答

50

好的,我仔细地研究了NSLayoutManager内部的运行机制,并根据反汇编进行了阅读。基于我的理解,它使用的代码可以归结为以下内容:

CGFloat ascent = CTFontGetAscent(theFont);
CGFloat descent = CTFontGetDescent(theFont);
CGFloat leading = CTFontGetLeading(theFont);

if (leading < 0)
  leading = 0;

leading = floor (leading + 0.5);

lineHeight = floor (ascent + 0.5) + floor (descent + 0.5) + leading;

if (leading > 0)
  ascenderDelta = 0;
else
  ascenderDelta = floor (0.2 * lineHeight + 0.5);

defaultLineHeight = lineHeight + ascenderDelta;

这将为您获取您上面提到的两种字体的111.0和115.0值。

我应该补充说明,根据OpenType规范,正确的方法只是添加三个值(如果您使用的API没有使它们全部为正数,请小心获得下降值的符号正确)。


即使行距为零,CoreText并不总是添加上升高度偏移量。我还没有弄清楚到底是什么决定了它是否添加。起初似乎在OpenType和TrueType之间有所不同,但我在两者中都找到了例外。 - mohsenr
从测试中可以发现,如果小于0,则领先者不会被设置为0。我还想指出,这个计算只适用于iOS6及之前的版本。 - thewormsterror
@thewormsterror 上面的代码基于回答撰写时 OS X 中的实际代码,因此如果它小于零,它实际上会被设置为 0。虽然这个问题是关于 OS X 的,而不是 iOS,但在 iOS 上,文本渲染系统是不同的,所以如果 iOS 使用的计算不同,我也不会感到惊讶。 - al45tair
@alastair 哦,是的,我很抱歉,尽管我认为在当时核心文本应该在iOS和OSX上使用相同的计算方法。 - thewormsterror
复活死亡的,但我想知道这些发现与任何事情有何关联... - andlabs

4

简单来说,您可以设置一个测试字符串和框架,并比较您想要的字体的两行文字的起点。然后,如果您需要计算行间距,只需使用行高、上标和下标来进行计算即可。

    - (float)getLineHeight {


        CFMutableAttributedStringRef testAttrString;
        testAttrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
        NSString *testString = @"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest";
        CFAttributedStringReplaceString (testAttrString, CFRangeMake(0, 0), (CFStringRef)testString);

        CTFontRef myFont1 = CTFontCreateWithName((CFStringRef)@"Helvetica", 30, NULL);
        CFRange range = CFRangeMake(0,testString.length);
        CFAttributedStringSetAttribute(testAttrString, range, kCTFontAttributeName, myFont1);

        CGMutablePathRef path = CGPathCreateMutable();
        CGRect bounds;
        if ([model isLandscape]) {
            bounds = CGRectMake(0, 10, 1024-20, 768);
        }
        else {
            bounds = CGRectMake(0, 10, 768-20, 1024);
        }    
        CGPathAddRect(path, NULL, bounds);

        CTFramesetterRef testFramesetter = CTFramesetterCreateWithAttributedString(testAttrString);
        CTFrameRef testFrameRef = CTFramesetterCreateFrame(testFramesetter,CFRangeMake(0, 0), path, NULL);
        CGPoint origins1,origins2;
        CTFrameGetLineOrigins(testFrameRef, CFRangeMake(0, 1), &origins1);
        CTFrameGetLineOrigins(testFrameRef, CFRangeMake(1, 1), &origins2);
        return origins1.y-origins2.y;
    }

太棒了,这个可行!我在子类化UITextView时进行了一些更改,并已经有了一个预先存在的CTFrameRef,这使事情变得更容易了一些,但这是一种始终可以准确计算任何字体、任何点大小和任何比例尺下的行高的好方法(对于那些需要考虑视图比例尺的人,只需添加一个CGFloat scale参数并将返回行更改为:return (origins1.y - origins2.y) * scale;)。非常感谢,user1307179! - chown

1

你有没有查看CTFontGetDescent()返回值的符号?一个常见的错误是假设下降值为正数,但实际上它们往往是负数(反映它们是字体基线以下的下降)。

因此,行间距可能应该设置为

ascent - descent + leading

1
我已经使用几种不同的字体进行了检查,CTFontGetDescent()始终返回正值。例如,我的示例代码中的字体下降值为20.77。 - Steven Vandeweghe
1
有趣,你说得对... 显然,Core Text会否定字体中'hhea'表中的值。 - al45tair

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接