Giving Framesetter the correct line spacing adjustment - ios

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:
Is “leading” really what is meant by kCTParagraphStyleSpecifierLineSpacingAdjustment?
If so, which method/formula should I use to get it?
If not, what should I use for the line spacing adjustment?

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;
}

Answers to the 3 questions I had above:
Yes, “leading” really what is meant by kCTParagraphStyleSpecifierLineSpacingAdjustment. Or at any rate, it works as expected.
Use CTFontGetLeading(fontRef) to get the font's normal leading, or plug in whatever value (as a CGFloat) you choose.
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:
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.
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.

Related

Inconsistent behavior of kerning in NSAttributedString

I need to use Kern attribute of NSAttributedString. As I can see in the documentation, default value of that attribute is 0.0. But I faced with strange behaviour for phrase Hello, world (for phrase "Hello" all fine):
NSDictionary<NSString*, id>* attributes = #{NSFontAttributeName: [UIFont systemFontOfSize:12]};
NSString* text = #"Hello, World";
NSAttributedString* string = [[NSAttributedString alloc] initWithString:text attributes:attributes];
CGSize size1 = [string size];
NSMutableDictionary<NSString*, id>* attributesWithKernel = [attributes mutableCopy];
attributesWithKernel[NSKernAttributeName] = #(0.0);
NSAttributedString* stringWithKern = [[NSAttributedString alloc] initWithString:text attributes:attributesWithKernel];
CGSize size2 = [stringWithKern size];
XCTAssertTrue(CGSizeEqualToSize(size1, size2)); //here test falls
//size1 = size1 = (width = 68.8125, height = 14.3203125)
//size2 = (width = 69.515625, height = 14.3203125)
To make size1 and size2 equal, kerning should be equal -7.105427357601002e-15 , I know that this is very close to 0.0, but it is strange, because this changes the width almost a pixel.
NSAttributedString has same behaviour in Objective-C and in Swift, example for swift:
let text = "Hello, World"
let attributes : [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: UIFont.systemFontSize)]
let str = NSAttributedString(string: text, attributes: attributes)
let size = str.size()
var attributesWithKern = attributes
attributesWithKern[NSAttributedString.Key.kern] = NSNumber(value: 0.0)
let strWithKern = NSAttributedString(string: text, attributes: attributesWithKern)
let sizeWithKern = strWithKern.size()
XCTAssertTrue(size == sizeWithKern)
How I can fix this behaviour?
P.S.
Now I just remove NSKernAttributeKey from an attribute string if the key equal to 0.0, but I do not think that it is a good solution.
I believe the docs here are wrong, and it is worth opening a radar about that. When no value is set, it is interpreted as "normal kerning." When 0 is set, it is interpreted as "disable kerning," which is why the width is a little wider (kerning typically is slightly negative, bringing kern-pair character, like "W" and "o" in this font, a little closer). I don't think there's any way to explicitly request "default kerning" without removing the attribute.
For your purposes, I believe you're doing the right thing by removing the value when it's zero, because you want default kerning, not to disable kerning.
The reason your tiny negative value is working is because it's not zero, so it's not disabling kerning, but it's so small that the behavior is very, very close to the default, and you're running into the precision of Double in the intermediate calculations (or possibly the precision of Float, depending on how it's implemented internally). You should find that your test passes for any value smaller (closer to zero) than this one, not just that value. In my tests, positive 7e-15 also works, for example.
I did some tests based on your code and I confirm this behaviour which looks like a bug. The point is for some letters strings are equal, for some not. For instance strings with "Hello Mo" or "Hello Oo" are equal, but for instance "Hello WoWoWo" differ a lot. So we see here some kerning being added for "W" without any reason. It can also depend on chosen font, I did not test it though. I see here the only solution that you've used - removing NSKernAttributeKey if it equals to 0.

How to properly draw underlines in NSLayoutManager

I have currently subclassed NSLayoutManager along with other classes in the UITextView architecture in order to implement my own Word Processor like UITextView. I am currently facing a problem when trying to layout underlines under ranges of glyphs (to implement something like hyperlink). In order to layout the underlines I have overrode the following method in NSLayoutManager.
- (void)drawUnderlineForGlyphRange:(NSRange)glyphRange underlineType (NSUnderlineStyle)underlineVal baselineOffset:(CGFloat)baselineOffset lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin
In the above method I compute the underline distance in points from origin of the underline to the end of the underline via the glyph range. View the following method:
- (void)getStartLocation:(CGFloat * _Nonnull )startLocation andEndLocation:(CGFloat * _Nonnull)endLocation ofGlyphsInRange:(NSRange)glyphRange {
NSRange characterRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
(*startLocation) = [self locationForGlyphAtIndex:glyphRange.location].x;
CGGlyph glyphs[self.numberOfGlyphs];
NSUInteger numGlyphs = [self getGlyphsInRange:glyphRange glyphs:&glyphs[0] properties:NULL characterIndexes:NULL bidiLevels:NULL];
CTFontRef ctfont = [self getCTFontForGlyphsInRange:glyphRange];
double advances = CTFontGetAdvancesForGlyphs(ctfont, kCTFontOrientationDefault, &glyphs[0], NULL, numGlyphs);
(*endLocation) = (*startLocation) + (CGFloat)advances;}
As you can see I get the startLocation, and then I get the endLocation by summing the advances for each glyph in the glyphRange. This computation seems to work reasonably well but it seems to result in a overdrawn or underdrawn line for some sequences of characters. It has recently come to my attention that CTFontGetAdvancesForGlyphs does NOT account for font kerning. Can anyone help me with this code? How can I incorporate font kerning effectively into my computation? I have tried querying the NSKernAttribute in my NSTextStorage at the given location but I keep getting back nil values. I have tried both getting the attributeAtIndex and tried enumerating for the attribute over a range.
I am also aware NSAttributedString has underline options but I want to draw the underline myself for various reasons such as supporting custom defined styles, colors, and more.

How to wrap text without showing hyphenation characters (while using the same word split rules)?

I am trying to achieve word wrapping with hyphenation but do not want to see the hyphenation character in the uitextview.
In viewDidLoad;
textFieldParagraphStyle.lineBreakMode = NSLineBreakMode.ByWordWrapping
textFieldParagraphStyle.alignment = NSTextAlignment.Center
textFieldParagraphStyle.hyphenationFactor = 1.0
Everything works well but is it possible to get rid of hyphenation character with preserving the correct syllable wrapping?
To illustrate, suppose we have the word understand. If I use word wrapping with hypenation factor set 1.0, the uitextview divided the word like;
under-
stand
What I want to achieve is;
under
stand
Here is a code snippet. Read about CFStringGetHyphenationLocationBeforeIndex here to understand how the method works. Hope it helps. If your text view width allows to have multiple words in a line, then probably you should define at what syllable in the word you should make hyphenation, you can change location and limitRange to get other hyphens, not only in the word's "middle", like in my example.
- (NSString*)hyphenatedWordFromWordString:(NSString*)word
{
CFStringRef strRef = (__bridge CFStringRef)word;
CFIndex location = _textView.text.length;
CFRange limitRange = CFRangeMake(0, location);
CFOptionFlags flags = 0;
CFLocaleRef locale = (__bridge CFLocaleRef)[NSLocale currentLocale];
CFIndex index = CFStringGetHyphenationLocationBeforeIndex(strRef, location, limitRange, flags, locale, NULL);
if (index != kCFNotFound) {
word = [NSString stringWithFormat:#"%#\n%#", [word substringToIndex:index], [word substringFromIndex:index]];
}
return word;
}

Prevent UILabel from character wrapping symbols

I have a multi-line label that has the following text:
Lots of text here · $$$$
Since the text at the beginning is freeform, sometimes the wrapping ends up looking like this:
Lots of text here · $$$
$
How do I prevent this from happening? I want it to look like this:
Lots of text here ·
$$$$
I've tried every lineBreakMode to little avail. Word wrap doesn't work because it doesn't treat $$$$ as a word.
It seems that you might benefit from subclassing UILabel, which would treat a string differently for the NSLineBreakByWordWrapping line break mode, which treats your phonetic words like words. You will effectively be expanding the definition by which your custom linebreakmode considers a word.
You would have to roll your own line-breaking algorithm. The approach to determining the location of your line-breaks would be similar to the following:
Loop through the string, to get each character, until one of two conditions is met: a) you have reached the width of the view, or b) you have reached a space, and the next word (delimited by a space) doesn't fit on the same line.
If you have reached condition a, you have two options--you could either adopt a policy that never splits words into multiple lines, or your could only apply the non-split rule to your phonetic words. Either way, you will need to insert a line break at the beginning of the phonetic word, when there is no more room on a given line.
You may want to use two separate strings, to keep the source string separate from the display string that contains your formatting.
Let me know if that helps!
This might be very late but atleast it could help someone.
The way I have done it is as follows:
UIFont *fontUsed = [UIFont systemFontOfSize:17];
NSDictionary *dictFont = [NSDictionary dictionaryWithObject:fontUsed forKey:NSFontAttributeName];
NSString *strTextToShow = #"text that has to be displayed but without $$$$";
CGRect rectForSimpleText = [strTextToShow boundingRectWithSize:CGSizeMake(154, 258) options:NSStringDrawingUsesLineFragmentOrigin attributes:dictFont context:nil];
NSString *strTextAdded = [NSString stringWithFormat:#"%# $$$$", strTextToShow];
CGFloat oldHeight = rectForSimpleText.size.height;
CGRect rectForAppendedText = [strTextAdded boundingRectWithSize:CGSizeMake(154, 258) options:NSStringDrawingUsesLineFragmentOrigin attributes:dictFont context:nil];
CGFloat newHeight = rectForAppendedText.size.height;
if (oldHeight < newHeight) {
strTextAdded = [strTextAdded stringByReplacingOccurrencesOfString:#"$$$$" withString:#"\n$$$$"];
}
[lblLongText setText:strTextAdded];
lblLongText here is the IBOutlet of UILabel and CGSizeMake(154, 258) is the size of UILabel I have used. Let me know if there is any other way you have found.
Try inserting a line break in your input text.
Lots of text here ·\n $$$
It should print the $$$ in the next line.

How to determine Line Spacing in pixels from a given font (for TVirtualTreeView)

I would like to determine real font line height based on font taken from system. The font I use is system icon font.
Here is my code so far.
LOGFONTW lf;
ZeroMemory(&lf, sizeof(lf));
// Get icon font size from the system
if (SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(lf), &lf, 0))
{
int H = 7;
{
// Create TBitmap and TFont
boost::scoped_ptr<Graphics::TBitmap> bmp(new Graphics::TBitmap);
boost::scoped_ptr<TFont> fnt(new TFont);
// Assign font from the system
fnt->Name = lf.lfFaceName;
fnt->Height = lf.lfHeight;
bmp->Canvas->Font->Assign(fnt.get());
// Calc height (returns 13 for default font size, 96 DPI but should be more like 18)
H = bmp->Canvas->TextHeight("Wq");
}
VST->DefaultNodeHeight = H;
VST->Font->Name = lf.lfFaceName;
VST->Font->Height = lf.lfHeight;
}
Now here is the problem. The above calculates text height which is 13 pixels for default font size at 96 DPI. But it should be 18 actually for nice pitch and line spacing. The difference increases as DPI is enlarged or font size is enlarged. If VirtualTreeView DefaultNoteHeight is set to 13 pixels it all looks very tight and lines are too close to each other.
What I need to know is actually line spacing as explained here:
http://msdn.microsoft.com/en-us/library/xwf9s90b%28v=VS.71%29.aspx
Please explain how do I extract line spacing from the given font from the system.
I believe GetTextMetrics holds the key but I just miss small piece of the puzzle to put it all together how to use it together with the above.
Examples in Delphi also welcome doesn't have to be in C++ Builder.
Update:
I've revised the formula a bit by adding:
H = bmp->Canvas->TextHeight("Wq");
// Take care of smaller heights to make them minimum 18 pixels
H = (H < 18)? 18 : H;
Seems to work OK for all font sizes I've tested with (for larger fonts it is a bit more tight but that's OK).
I have used this code to calculate the rect needed to display a block of text, it might be of some use to you:
It's an adaptation from my original code, but it should give you the idea: DT_CALCRECT is the flag to set so it returns the rect needed.
RECT rc_ancho_en_gui;
int height = 0;
rc_ancho_en_gui.left = 0;
rc_ancho_en_gui.right = 100;
rc_ancho_en_gui.top = 0;
rc_ancho_en_gui.bottom = 100;
height = DrawText(BitmapHandle, "Wq", 2, &rc_ancho_en_gui, DT_CALCRECT | DT_WORDBREAK);
I used before TextHeight but it didn't work fine, either giving me more or less than what was really needed. This way I haven't had any issues with the resulting height.

Resources