Giving Framesetter the correct line spacing adjustment
Asked Answered
F

2

3

Several posts have noted difficulties with getting an exact height out of CTFramesetterSuggestFrameSizeWithConstraints, and here, (framesetter post), @Chris DeSalvo gives what looks like the definitive fix: add a paragraph style setting with the correct line spacing adjustment.

DeSalvo gets his “leading” by removing UIFont’s ascender and descender from its lineHeight. I wondered how that would compare to CTFontGetLeading.

I worked with fonts created like this:

CTFontRef fontr = CTFontCreateWithName((CFStringRef)@"Helvetica Neue", 16.0f, NULL);
UIFont *font = [UIFont fontWithName:@"Helvetica Neue" size:16.0f];

The values were quite different:

  • 0.448 CTFontGetLeading
  • 2.360 DeSalvo’s formula: UIFont lineHeight - ascender + descender

Here are some other UIFont values:

  • 21.000 UIFont’s lineHeight
  • 15.232 UIFont’s ascender (Y coord from baseline)
  • -3.408 UIFont’s descender (Y coord from baseline)
  • 08.368 UIFont’s xHeight

And here are the CTFont values that Ken Thomases inquired about:

  • 11.568001 CTFontGetCapHeight
  • 08.368 CTFontGetXHeight
  • -15.216001, -7.696001, 38.352001, 24.928001 CTFontGetBoundingBox
  • 15.232 CTFontGetAscent
  • 03.408 CTFontGetDescent (class ref says "scaled font-descent metric scaled according to the point size and matrix of the font reference" -- which apparently means that it is the absolute value of the Y coordinate from the baseline?)

I note that UIFont previously had a property specifically for “leading,” but it has been deprecated and we are advised to use lineHeight instead. So UIFont considers leading to be 21 and CTFontRef .448 for the same font? Something’s not right.

Three questions:

  1. Is “leading” really what is meant by kCTParagraphStyleSpecifierLineSpacingAdjustment?
  2. If so, which method/formula should I use to get it?
  3. If not, what should I use for the line spacing adjustment?
Furring answered 8/4, 2012 at 23:23 Comment(9)
I don't think it was suggested to use lineHeight as a drop-in replacement for leading. I think the suggestion was to stop caring about leading and think in terms of line height, instead. That said, I can't reconcile the difference between CTFontGetLeading and the formula. What do the other CTFontGet... functions give (ascent, bounding box height, descent, cap height, x height)?Horwitz
Good idea. I've added those CTFontGet values to the question. I don't see any combos that would yield 0.448. And I wonder how we could think in terms of line height when considering inter-line spacing, particularly when line height is defined simply as "height of text lines"?Furring
Have you tried setting kCTParagraphStyleSpecifierMinimumLineHeight instead of the line space adjustment? At least you have a certain value for that.Horwitz
I'm just now putting together the implementation. I was previously using UIKit's NSString method sizeWithFont:, which is tricky due to its disregard of line breaks, and was advised I really should switch to this framesetter method. I want exact line height, not max or min, so that I can calculate how tall the textLayer and UIScrollView's contentSize will be, and so that I can scroll to newly added lines.Furring
Well, LineSpacingAdjustment wouldn't give you an exact line height, either. As its name suggests, it's an adjustment to whatever the framesetter computes from the font. If you want a constant line height, then set both MinimumLineHeight and MaximumLineHeight. I'm not familiar with the iOS text view architecture, but I would be surprised if it didn't provide a means for scrolling a character position or range into view. For example, Mac OS X's NSTextView has -scrollRangeToVisible: inherited from NSText.Horwitz
If you take a look at the post I link to at the top (the question and DeSalvo's answer), the issue will be more clear.Furring
I did look at that before I ever commented on your question. I'm proposing an alternative solution. DeSalvo suggested setting LineSpacingAdjustment, I'm suggesting MinimumLineHeight and MaximumLineHeight. I'm also suggesting that, if you were only using CTFramesetter for a way to "scroll to newly added lines", then you could use -[UITextView scrollRangeToVisible:].Horwitz
The unspoken issue here, the reason for all the roundabout methods, is that this involves attributed strings. UITextView, unlike its far more robust counterpart NSTextView, does not take attributed strings. The only way to work with attributed strings in iOS is to put them in a CATextLayer and use CoreText classes, like the framesetter discussed here, to deal with sizing. (Or UIKit's sizeWithFont:, which has other problems -- see [#9929244 -- which led me to framesetter.Furring
The plot thickens. Per Dondragmer's answer in this post [#10071698, CATextLayer will ignore the spacing adjustments. But per the same post, there's a way to draw the string directly to a plain CALayer. But then I can't use CATextLayer's wrapping behavior, and must do my own line breaks. Sheesh.Furring
F
0

Answers to the 3 questions I had above:

  1. Yes, “leading” really what is meant by kCTParagraphStyleSpecifierLineSpacingAdjustment. Or at any rate, it works as expected.
  2. Use CTFontGetLeading(fontRef) to get the font's normal leading, or plug in whatever value (as a CGFloat) you choose.
  3. N/A.

Answers 1 and 2 work: Specifying a leading value in a paragraphStyle attribute of your attributed string will enable the Core-Text framesetter to calculate its height exactly.

There are two caveats:

  1. If you try to calculate heights incrementally, one string at a time, each string containing an initial line break, framesetter will consider that line break to represent an entire line, not just the leading. If you want the height of the concatenated strings, you have to feed that concatenation to the framesetter. Of course, you could keep track of the incremental height differences, but there's no way to avoid having framesetter recalculate the earlier string dimensions.
  2. CATextLayer ignores spacing adjustments (and other attributes). If framing per exact string height is an issue, you must draw direct to a CALayer.

And there is one mystery: What is going on with UIFont's deprecated leading? Leading and lineHeight are two distinct things.

Furring answered 14/4, 2012 at 17:11 Comment(0)
F
2

I too ran into this and here is the code that worked in a real project:

// When you create an attributed string the default paragraph style has a leading 
// of 0.0. Create a paragraph style that will set the line adjustment equal to
// the leading value of the font. This logic will ensure that the measured
// height for a given paragraph of attributed text will be accurate wrt the font.

- (void) applyParagraphAttributes:(CFMutableAttributedStringRef)mAttributedString
{
  CGFloat leading = CTFontGetLeading(self.plainTextFont);

  CTParagraphStyleSetting paragraphSettings[1] = {
    kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof (CGFloat), &leading
  };

  CTParagraphStyleRef  paragraphStyle = CTParagraphStyleCreate(paragraphSettings, 1);

  CFRange textRange = CFRangeMake(0, [self length]);

  CFStringRef keys[] = { kCTParagraphStyleAttributeName };
  CFTypeRef values[] = { paragraphStyle };

  CFDictionaryRef attrValues = CFDictionaryCreate(kCFAllocatorDefault,
                                                  (const void**)&keys,
                                                  (const void**)&values,
                                                  sizeof(keys) / sizeof(keys[0]),
                                                  &kCFTypeDictionaryKeyCallBacks,
                                                  &kCFTypeDictionaryValueCallBacks);

  BOOL clearOtherAttributes = FALSE;
  CFAttributedStringSetAttributes(mAttributedString, textRange, attrValues, (Boolean)clearOtherAttributes);
  CFRelease(attrValues);

  CFRelease(paragraphStyle);

  self.stringRange = textRange;

  return;
}
Fulviah answered 28/6, 2013 at 22:12 Comment(0)
F
0

Answers to the 3 questions I had above:

  1. Yes, “leading” really what is meant by kCTParagraphStyleSpecifierLineSpacingAdjustment. Or at any rate, it works as expected.
  2. Use CTFontGetLeading(fontRef) to get the font's normal leading, or plug in whatever value (as a CGFloat) you choose.
  3. N/A.

Answers 1 and 2 work: Specifying a leading value in a paragraphStyle attribute of your attributed string will enable the Core-Text framesetter to calculate its height exactly.

There are two caveats:

  1. If you try to calculate heights incrementally, one string at a time, each string containing an initial line break, framesetter will consider that line break to represent an entire line, not just the leading. If you want the height of the concatenated strings, you have to feed that concatenation to the framesetter. Of course, you could keep track of the incremental height differences, but there's no way to avoid having framesetter recalculate the earlier string dimensions.
  2. CATextLayer ignores spacing adjustments (and other attributes). If framing per exact string height is an issue, you must draw direct to a CALayer.

And there is one mystery: What is going on with UIFont's deprecated leading? Leading and lineHeight are two distinct things.

Furring answered 14/4, 2012 at 17:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.