How to properly draw underlines in NSLayoutManager - ios

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.

Related

Get hash color string from image in objective c

Hi Can we get hash color string from UIImage ?
In below method if i pass [UIColor redColor] it is working , but if i pass
#define THEME_COLOR [UIColor colorWithPatternImage:[UIImage imageNamed:#"commonImg.png"]]
then it is not working.
+(NSString *)hexValuesFromUIColor:(UIColor *)color {
if (CGColorGetNumberOfComponents(color.CGColor) < 4) {
const CGFloat *components = CGColorGetComponents(color.CGColor);
color = [UIColor colorWithRed:components[0] green:components[0] blue:components[0] alpha:components[1]];
}
if (CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)) != kCGColorSpaceModelRGB) {
return [NSString stringWithFormat:#"#FFFFFF"];
}
return [NSString stringWithFormat:#"#%02X%02X%02X", (int)((CGColorGetComponents(color.CGColor))[0]*255.0), (int)((CGColorGetComponents(color.CGColor))[1]*255.0), (int)((CGColorGetComponents(color.CGColor))[2]*255.0)];
}
Is there any other methods which can directly get Hash color from UIImage ?
You can't access the raw data directly, but by getting the CGImage of this image you can access it. Reference Link
You can't do it directly from the UIImage, but you can render the image into a bitmap context, with a memory buffer you supply, then test the memory directly. That sounds more complex than it really is, but may still be more complex than you wanted to hear.
If you have Erica Sadun's iPhone Developer's Cookbook there's good coverage of it from page 54. I'd recommend the book overall, so worth getting that if you don't have it.
I arrived at almost exactly the same code independently, but hit one bug that it looks like may be in Sadun's code too. In the pointInside method the point and size values are floats and are multiplied together as floats before being cast to an int. This is fine if your coordinates are discreet values, but in my case I was supplying sub-pixel values, so the formula broke down. The fix is easy once you've identified the problem, of course - just cast each coordinate to an int before multiplying - so, in Sadun's case it would be:
long startByte = ((int)point.y * (int)size.width) + (int)point.x) * 4;
Also, Sadun's code, as well as my own, are only interested in alpha values, so we use 8 bit pixels that take the alpha value only. Changing the CGBitMapContextCreate call should allow you to get actual colour values too (obviously if you have more than 8 bits per pixel you will have to multiply that in to your pointInside formula too).
OR

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 do I fold text in iOS 7?

I feel like an idiot not even posting some code, but after reading several articles stating iOS7 Text Kit adds support for Text Folding, I can't actually find any sample code or an attribute to set on the text to fold it and Apple's documentation seems mute on it.
http://asciiwwdc.com/2013/sessions/220 makes me think I set a region of text into its own text container and then display/hide it, perhaps by overriding setTextContainer:forGlyphRange:
Am I anywhere close?
Thanks
There's a WWDC 2013 video that talks a bit about it when they're doing custom text truncation. Basically you implement the NSLayoutManagerDelegate method layoutManager: shouldGenerateGlyphs: properties: characterIndexes: font: forGlyphRange:
It took me way too much struggling to actually come up with code for this, but here's my implementation based on a property hideNotes
-(NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs
properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes
font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange {
if (self.hideNotes) {
NSGlyphProperty *properties = malloc(sizeof(NSGlyphProperty) * glyphRange.length);
for (int i = 0; i < glyphRange.length; i++) {
NSUInteger glyphIndex = glyphRange.location + i;
NSDictionary *charAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:NULL];
if ([[charAttributes objectForKey:CSNoteAttribute] isEqualToNumber:#YES]) {
properties[i] = NSGlyphPropertyNull;
} else {
properties[i] = props[i];
}
}
[layoutManager setGlyphs:glyphs properties:properties characterIndexes:charIndexes font:aFont forGlyphRange:glyphRange];
return glyphRange.length;
}
[layoutManager setGlyphs:glyphs properties:props characterIndexes:charIndexes font:aFont forGlyphRange:glyphRange];
return glyphRange.length;
}
The NSLayoutManager method setGlyphs: properties: characterIndexes: font: forGlyphRange: is called in the default implementation and basically does all of the work. The return value is the number of glyphs to actually generate, returning 0 tells the layout manager to do its default implementation so I just return the length of the glyph range it passes in. The main part of the method goes through all of the characters in the text storage and if it has a certain attribute, sets the associated property to NSGlyphPropertyNull which tells the layout manager to not display it, otherwise it just sets the property to whatever was passed in for it.

Giving Framesetter the correct line spacing adjustment

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.

iOS: Algorithm to center a word in a sentence inside of a UILabel

I need to center a particular word in a sentence by truncating the beginning and endings of a long sentence inside of a UILabel, For example
NSString mySentence = #"This is my very long sentence to give you an example of what I am trying to do.. And its still going..";
NSString myWord = #"example";
<Algorithm goes here>
Should display:
"...sentence to give you an example of what I am trying to..."
If the word is closer to one end or the other, just do your best to center and display an appropriate amount of the text, example:
NSString myWord = #"my";
"This is my very long sentence to give you an example of what..."
Any ideas?
Thanks.
I believe you can do a scan for your search to be placed. Let's say you have the index in a variable named centerWordIndex. You could then split the string based on whitechars, and add words to the beginning and the end of your word until you are out of words at each side, or until the size of you string matches the size of the label.
what do you think :) ?
The data you need to start is:
the width of your label (call it labelWidth)
the width of the word you want to center (call it wordWidth)
Then the size on each side you have to work with is (labelWidth - wordWidth) / 2. Don't worry about using this value, it's just the target.
You can use the ability to calculate the size of a NSString using
CGSize newSize = [myString sizeWithFont: myFont];
CGFloat newWidth = newSize.width;
The goal of the algorithm should be to continue to add words on each side of the centered word and recalculating the total width each step. If you've gone past labelWidth, you can't add that word or anymore from that direction. So, in pseudocode, one approach is:
calculate labelWidth and wordWidth
set currentWidth to wordWidth
set currentLeftPosition to position of first letter of word
set currentRightPosition to position of last letter of word
set currentString to word
set currentImbalance to 0
algorithm start:
scan for position of 2 spaces to left of currentLeftPosition or start of string
set leftTrialPosition to position found
set leftTrialString as between leftTrialPosition and currentRightPosition inclusive
calculate trialLeftWidth of leftTrialString
scan for position of 2 spaces to right of currentRightPosition or end of string
set rightTrialPosition to position found
set rightTrialString as between currentLeftPosition and rightTrialPositon inclusive
calculate trialRightWidth of rightTrialString
if (trialLeftWidth - currentImbalance <= trialRightWidth
&& trialLeftWidth <= labelWidth
&& trialLeftWidth != currentWidth)
set currentImbalance -= calculate width of string from leftTrialPosition to currentLeftPosition
set currentLeftPosition = leftTrialPosition
set currentWidth = trialLeftWidth
set currentString = leftTrialString
else if (same checks for right)
same steps using right data
else if (both left and right are larger than label or both sides no longer grow bigger)
algorithm is done - return here
recurse to algorithm start
Using this basic strategy, you track the left imbalance (negative) or right imbalance (positive) throughout the algorithm and preferentially add the left or right word accordingly until you have the biggest string of complete words that can fit on the label or you have used the full string. The key iOS specific part here is the NSString method that calculates the width.

Resources