I have a view in which I want to draw a text with Text Core (on the iPad). When text grown up I'd like to increase a height of the view, but I don't know how to calculate needed height of frame.
I use it to draw a text in drawRect method:
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)currentTexts);
CGMutablePathRef textPath = CGPathCreateMutable();
CGRect textRect = CGRectMake(PADDING, PADDING, self.frame.size.width - 2 * PADDING, self.frame.size.height - 2 * PADDING);
CGPathAddRect(textPath, NULL, textRect);
CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), textPath, NULL);
CTFrameDraw(textFrame, context);
CFRelease(textFrame);
CGPathRelease(textPath);
CFRelease(framesetter);
I tried to get a height of text using sizeWithFont and also that:
- (CGSize) measureFrame: (CTFrameRef) frame
{
CGPathRef framePath = CTFrameGetPath(frame);
CGRect frameRect = CGPathGetBoundingBox(framePath);
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex numLines = CFArrayGetCount(lines);
CGFloat maxWidth = 0;
CGFloat textHeight = 0;
// Now run through each line determining the maximum width of all the lines.
// We special case the last line of text. While we've got it's descent handy,
// we'll use it to calculate the typographic height of the text as well.
CFIndex lastLineIndex = numLines - 1;
for(CFIndex index = 0; index < numLines; index++)
{
CGFloat ascent, descent, leading, width;
CTLineRef line = (CTLineRef) CFArrayGetValueAtIndex(lines, index);
width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
if(width > maxWidth)
{
maxWidth = width;
}
if(index == lastLineIndex)
{
// Get the origin of the last line. We add the descent to this
// (below) to get the bottom edge of the last line of text.
CGPoint lastLineOrigin;
CTFrameGetLineOrigins(frame, CFRangeMake(lastLineIndex, 1), &lastLineOrigin);
// The height needed to draw the text is from the bottom of the last line
// to the top of the frame.
textHeight = CGRectGetMaxY(frameRect) - lastLineOrigin.y + descent;
}
}
// For some text the exact typographic bounds is a fraction of a point too
// small to fit the text when it is put into a context. We go ahead and round
// the returned drawing area up to the nearest point. This takes care of the
// discrepencies.
return CGSizeMake(ceil(maxWidth), ceil(textHeight));
}
I use that to create an attrubuted string:
CTParagraphStyleSetting setting[1] = {
{kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &minimumLineSpacing}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(setting, 1);
NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:
(id)textColor.CGColor, kCTForegroundColorAttributeName,
(id)currentFont, kCTFontAttributeName,
(id)paragraphStyle, kCTParagraphStyleAttributeName,
nil];
When I use sizeWithFont, at the begin everything is ok, but when text has more lines, the frame is bigger and bigger than a text and I want it to fit exactly the text. How can I make it?
To calculate the height of the text, have you tried using CTFramesetterSuggestFrameSizeWithConstraints ?
http://foobarpig.com/iphone/using-ctframesettersuggestframesizewithconstraints-and-sizewithfont-to-calculate-text-height.html
Related
I want to draw an array of strings of varied font size on a UIView as this.I have calculated the height required for each string to be drawn. The array of string in the example is {"Enter some text", "that", "will fit", "the rect"}. I calculate the height as below,
#implementation NSString (Additions)
- (CGFloat)heightForFont:(UIFont *)font {
NSAttributedString *attrString = [[NSAttributedString alloc]initWithString:self attributes:#{NSFontAttributeName:font}];
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString);
CGFloat ascent, descent, leading;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent + leading;
return height;
}
Then I try to draw the string in a UIView as follows:
- (void)drawStringArray:(NSArray *)splittedStringArray maxYoFString:(CGFloat)maxY rect:(CGRect)rect fontsDict:(NSMutableDictionary *)fontsDict {
//Alter X origin if scaled
CGFloat originX = rect.origin.x;
CGRect drawRect = rect;
drawRect.origin.x = originX;
drawRect.origin.y = 0.0;
CGFloat heightForString = 0.0;
for (int i = 0; i < splittedStringArray.count; i++) {
NSString *string = splittedStringArray[i];
UIFont *font = [fontsDict objectForKey:string];
if(i < splittedStringArray.count -1)
heightForString = [string heightForFont:font];
[string drawInRect:rect withAttributes:#{
NSFontAttributeName : font,
NSForegroundColorAttributeName :self.random.currentTextColor
}];
drawRect.origin.y += heightForString;
}
}
But there is always a space that gets added between two lines. I want to draw the string without any leading space.current image, expected image
An easy solution would be to patch the font file itself and fix its line height definitely. Check out how to do it, here.
http://mbauman.net/geek/2009/03/15/minor-truetype-font-editing-on-a-mac/
I'm using this to generate a curved text:
- (UIImage*)createCircularText:(NSString*)text withSize:(CGSize)size andCenter:(CGPoint)center
{
UIFont *font = [UIFont fontWithName:#"HelveticaNeue-Light" size:15];
// Start drawing
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
// Retrieve the center and set a radius
CGFloat r = center.x / 3;
// Start by adjusting the context origin
// This affects all subsequent operations
CGContextTranslateCTM(context, center.x, center.y);
// Calculate the full extent
CGFloat fullSize = 0;
for (int i = 0; i < [text length]; i++)
{
NSString *letter = [text substringWithRange:NSMakeRange(i, 1)];
CGSize letterSize = [letter sizeWithAttributes:#{NSFontAttributeName:font}];
fullSize += letterSize.width;
}
// Initialize the consumed space
CGFloat consumedSize = 0.0f;
// Iterate through the alphabet
for (int i = 0; i < [text length]; i++)
{
// Retrieve the letter and measure its display size
NSString *letter = [text substringWithRange:NSMakeRange(i, 1)];
CGSize letterSize = [letter sizeWithAttributes:#{NSFontAttributeName:font}];
// Calculate the current angular offset
//CGFloat theta = i * (2 * M_PI / ((float)[text length] * 3));
// Move the pointer forward, calculating the new percentage of travel along the path
consumedSize += letterSize.width / 2.0f;
CGFloat percent = consumedSize / fullSize;
CGFloat theta = (percent * 2 * M_PI) / ((float)[text length] / 4);
consumedSize += letterSize.width / 2.0f;
// Encapsulate each stage of the drawing
CGContextSaveGState(context);
// Rotate the context
CGContextRotateCTM(context, theta);
// Translate up to the edge of the radius and move left by
// half the letter width. The height translation is negative
// as this drawing sequence uses the UIKit coordinate system.
// Transformations that move up go to lower y values.
CGContextTranslateCTM(context, -letterSize.width / 2, -r);
// Draw the letter and pop the transform state
[letter drawAtPoint:CGPointMake(0, 0) withAttributes:#{NSFontAttributeName:font}];
CGContextRestoreGState(context);
}
// Retrieve and return the image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
and i get this back:
The problem is that the text starts at 0° but I actually want it to begin more at the left, so that the center of the string is at 0°. How to accomplish this?
Two options should work:
After drawing all of the text, rotate the context half of the use angle and get the image from the context at that point.
Make two passes. The first simply calculates the required angle to draw the text. The second pass does what you do now. Except in the 2nd pass, subtract half of the required total angle from each letter's angle.
I am decomposing a multiline string into word boundaries on iOS. My solution centers around the boundingRectForGlyphRange method of NSLayoutManager. It ALMOST works, except that the rect for each word is a few pixels off to the right. In other words NSLayoutManager seems to be adding a leading space / indent on each line and I cannot find any way to override this behavior.
I tried using NSLayoutManager.usesFontLeading as well as NSParagraphStyle.headIndent but without any results:
NSLayoutManager* layout = [NSLayoutManager new];
layout.usesFontLeading = NO;
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.headIndent = paragraphStyle.firstLineHeadIndent = 0;
NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:self attributes:#{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
layout.textStorage = textStorage;
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:size];
[layout addTextContainer:textContainer];
// compute bounding rect for each word range
for (NSValue* wordRangeValue in wordRanges)
{
NSRange wordRange = [wordRangeValue rangeValue];
NSRange wordGlyphRange = [layout glyphRangeForCharacterRange:wordRange actualCharacterRange:NULL];
CGRect wordBounds = [layout boundingRectForGlyphRange:wordGlyphRange inTextContainer:textContainer];
}
Screenshot: the gray rectangles represent label bounds, red rectangles represent text rect for each label and computed word boundaries from the [boundingRectForGlyphRange:] method above. Notice that the computed word boundaries are off by a few pixels.
I am also open to other methods for computing word boundaries, but boundingRectForGlyphRange seems very convenient for my purpose.
To ignore the left margin, use:
textContainer.lineFragmentPadding = 0;
I was not able to force NSLayoutManager to omit the left margin. If anyone knows how to get NSLayoutManager to ignore the left margin, let me know.
My workaround was to use Core Text instead. This was MUCH more difficult and involved. My solution does not lend itself to pasting into a single code excerpt, but this should give you a good reference if you want to go the same route:
- (NSArray*) coreTextLayoutsForCharacterRanges:(NSArray*)ranges withFont:(UIFont *)font constrainedToSize:(CGSize)size{
// initialization: make frame setter
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; // watch out - need to specify line wrapping to use multi line layout!
paragraphStyle.minimumLineHeight = font.lineHeight; // watch out - custom fonts do not compute properly without this!
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:self attributes:#{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFRange wholeString = CFRangeMake(0, self.length);
CGRect bounds = CGRectMake(0, 0, size.width, size.height);
CGMutablePathRef boundsPath = CGPathCreateMutable();
CGPathAddRect(boundsPath, NULL, bounds);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, wholeString, boundsPath, NULL);
CFRelease(boundsPath);
// extract lines
CFArrayRef lines = CTFrameGetLines(frame);
int lineCount = CFArrayGetCount(lines);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
NSMutableArray* lineLayouts = [NSMutableArray arrayWithCapacity:lineCount];
CGFloat h = size.height;
for (int i = 0; i < lineCount; ++i){
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CGPoint lineOrigin = lineOrigins[i]; // in Core Graphics coordinates! let's convert.
lineOrigin.y = h - lineOrigin.y;
TextLayout* lineLayout = [[CTLineLayout alloc] initWithString:self line:line lineOrigin:lineOrigin];
[lineLayouts addObject:lineLayout];
}
// got line layouts. now we iterate through the word ranges to find the appropriate line for each word and compute its layout using the corresponding CTLine.
Another important part is how to get the bounding rect of a word in a line by using CTLine. I factored this into a CTLineLayout module, but the gist is this (the 'origin' variable refers to the line origin computed in the code sample above):
CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat top = origin.y - ascent;
CGFloat bottom = origin.y + descent;
CGRect result = CGRectMake(origin.x, top, width, bottom - top); // frame of entire line (for now)
CGFloat left = CTLineGetOffsetForStringIndex(line, stringRange.location, NULL);
CGFloat right = CTLineGetOffsetForStringIndex(line, NSMaxRange(stringRange), NULL);
result.origin.x = left;
result.size.width = right - left; // frame of target word in UIKit coordinates
The above is a rough excerpt - I factored CTLine to compute the line's bounds once in the initializer, then compute only the left + right endpoints when getting the frame of a word.
Whew!
I want to calculate the height of a NSAttributedString with the NSParagraphStyle attribute.
I thought it will be easy to create a UILabel with higher spacing between the lines but i can't calculate the right height for my UITableViewCell.
I tried to calculate it with boundingRectWithSize:options: but it's not working at all…
I use NSLayoutManager's usedRectForTextContainer: with a TextView stack disconnected from a UITableView. I answered a similar Stack Overflow question and explained how to implement it.
When the convinient methods from apple dont work, this category provides a good aproximation in most cases.
#implementation NSAttributedString (PixLib)
- (CGFloat)heightForWidth:(CGFloat)width {
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, width, 99999));
CGFloat h = [self heightForPath:path];
CGPathRelease(path);
return h;
}
- (CGFloat)heightForPath:(CGPathRef)path {
CGFloat height = 0;
CTFrameRef frame = [self cfframeForPath:path];
if (frame != NULL) {
NSArray* lines = (__bridge NSArray*)CTFrameGetLines(frame);
int l = [lines count];
if (l > 1) {
CGPoint origins[l];
CTFrameGetLineOrigins(frame, CFRangeMake(0, l), origins);
CGFloat yFirst = origins[0].y;
CGFloat yLast = origins[l-1].y;
CGFloat ascent, descent, leading;
CTLineGetTypographicBounds((__bridge CTLineRef)[lines objectAtIndex:l-1], &ascent, &descent, &leading);
height = ceilf((ascent+descent+leading)*1.3) + yFirst-yLast;
} else {
if (l==1) {
CGFloat ascent, descent, leading;
CTLineGetTypographicBounds((__bridge CTLineRef)[lines objectAtIndex:0], &ascent, &descent, &leading);
height = ceilf(ascent+descent+leading)*1.3;
}
}
CFRelease(frame);
}
return height;
}
- (CTFrameRef)cfframeForPath:(CGPathRef)p {
// hack to avoid bugs width different behavior in iOS <4.3 and >4.3
CGMutablePathRef path = CGPathCreateMutable();
CGRect r = CGPathGetBoundingBox(p);
CGAffineTransform t = CGAffineTransformIdentity;
t = CGAffineTransformTranslate(t, r.origin.x, r.origin.y);
t = CGAffineTransformScale(t, 1, -1);
t = CGAffineTransformTranslate(t, r.origin.x, - ( r.origin.y + r.size.height ));
CGPathAddPath(path, &t, p);
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathCloseSubpath(path);
// hack end
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)self);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFRelease(framesetter);
CGPathRelease(path);
return frame;
}
#end
I'm work with EGOTextView.I set the textContentView for viewForZoomingInScrollView.when I zoom it out and typing.It sometimes returns a incorrect height. Here is what I am doing:
CTFramesetterRef framesetter = _framesetter;
CFAttributedStringRef attributedStringRef=(CFAttributedStringRef)self.attributedString;
_framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);
if (framesetter!=NULL) {
CFRelease(framesetter);
}
CGRect rect = self.textContextView.frame;
CGFloat height = [self heightForAttributedString:self.attributedString forWidth:rect.size.width];
rect.size.height = (height + self.font.lineHeight * 2) * pageScale;
//pageScale is the scale scrollview end zooming
- (CGFloat)boundingHeightForWidth:(CGFloat)width {
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(_framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(width , CGFLOAT_MAX), NULL);
return suggestedSize.height;
}
And I try this method too:
+(CGFloat)heightForAttributedString:(NSAttributedString *)attrString forWidth:(CGFloat)inWidth
{
CGFloat H = 0;
// Create the framesetter with the attributed string.
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attrString);
CGRect box = CGRectMake(0,0, inWidth, CGFLOAT_MAX);
CFIndex startIndex = 0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, box);
// Create a frame for this column and draw it.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
CFArrayRef lineArray = CTFrameGetLines(frame);
CFIndex j = 0, lineCount = CFArrayGetCount(lineArray);
CGFloat h, ascent, descent, leading;
for (j=0; j < lineCount; j++)
{
CTLineRef currentLine = (CTLineRef)CFArrayGetValueAtIndex(lineArray, j);
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading);
h = ascent + descent + leading;
NSLog(#"%f", h);
H+=h;
}
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
return H;
}
But it seems always return a wrong height when you typing text after zoom.
Is there any other/better way to figure out the correct height for my attributed string? Thanks!
I also run in this case, solved it and wrote a category, which works for me in all cases. See my answer in If you use zero as height, it does not restrict the height to any height. I wrote a category for this issue, which works for me in all cases. See iOS UITableView with dynamic text and images rendered together (NSAttributedString + images)