Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does line spacing work in Core Text? (and why is it different from NSLayoutManager?)

I'm trying to draw text using Core Text functions, with a line spacing that's as close as possible to what it would be if I used NSTextView.

Take this font as an example:

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

The line height of this font, if I would use it in an NSTextView is 111.0.

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

Now, if I do the same thing with Core Text, the result is 110.4 (assuming you can calculate the line height by adding the ascent, descent and leading).

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

This is very close to 111.0, but for some fonts the difference is much bigger. E.g. for Helvetica, NSLayoutManager gives 115.0 whereas CTFont ascent + descent + leading = 96.0. Clearly, for Helvetica, I wouldn't be able to use ascent + descent + leading to calculate the spacing between lines.

So I thought I'd use CTFrame and CTFramesetter to layout a few lines and get the linespacing from that. But that also gives different values.

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 

So the line spacing is now even more different from the 111.0 that was used in my NSTextView, and not every line is equal. It seems that the line breaks add some extra space (even though the default value for paragraphSpacingBefore is 0.0).

I'm working around this problem now by getting the line height via NSLayoutManager and then individually drawing each CTLine, but I wonder if there's a better way to do this.

like image 732
Steven Vandeweghe Avatar asked Apr 01 '11 10:04

Steven Vandeweghe


People also ask

What is the spacing between lines of text Why is this important in typography?

The traditional term for line spacing is leading (rhymes with bedding ), so named because traditional print shops put strips of lead between lines of type to increase vertical space. Sometimes you see this term in typesetting software. For most text, the optimal line spacing is between 120% and 145% of the point size.

Why is line spacing important?

Too large and lines will appear unrelated and spacey. Line spacing also impacts accessibility. Users with cognitive disabilities or visual impairment have trouble tracking lines of text when too closely spaced. Providing the right balance of line spacing allows users to move down to the next line of text more easily.

What is the space between lines of text?

In typography, leading (/ˈlɛdɪŋ/ LED-ing) is the space between adjacent lines of type; the exact definition varies. In hand typesetting, leading is the thin strips of lead (or aluminium) that were inserted between lines of type in the composing stick to increase the vertical distance between them.

When line spacing is _ the text is difficult to read?

It becomes much easier to read when line spacing is increased, making it 14/21 point. When text is set solid (upper) – even at a generous point size – it can be difficult to read, as illustrated in this setting of the Metro® Nova typeface at 18/18.


2 Answers

OK, so I took a good look at what goes on in the guts of NSLayoutManager, and it appears, based on my reading of the disassembly, that the code it uses boils down to something like this:

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; 

This will get you the 111.0 and 115.0 values for the two fonts you mention above.

I should add that the correct way, according to the OpenType specification, is just to add the three values (being careful, if you’re using an API that doesn’t make them all positive, to get the sign of the descent value correct).

like image 88
al45tair Avatar answered Sep 25 '22 17:09

al45tair


simple. set up a test string and frame and compare origin of two lines of the font you want. Then if you want to calculate leading just use line height accent descent to do the calculation.

    - (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;     } 
like image 35
user1307179 Avatar answered Sep 25 '22 17:09

user1307179