Core Text calculates letter frame in iOS

I need to calculate the exact bounding box for each character (glyph) in NSAttributedString (Core Text). After composing some code used to solve such problems (choosing the main text, etc.), the result is not bad, but only a few frames (red) are calculated correctly:

enter image description here

Most frames fit both horizontally and vertically (a tiny bit). What is the reason for this? How can I improve this code ?:

-(void)recalculate{ // get characters from NSString NSUInteger len = [_attributedString.string length]; UniChar *characters = (UniChar *)malloc(sizeof(UniChar)*len); CFStringGetCharacters((__bridge CFStringRef)_attributedString.string, CFRangeMake(0, [_attributedString.string length]), characters); // allocate glyphs and bounding box arrays for holding the result // assuming that each character is only one glyph, which is wrong CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph)*len); CTFontGetGlyphsForCharacters(_font, characters, glyphs, len); // get bounding boxes for glyphs CTFontGetBoundingRectsForGlyphs(_font, kCTFontDefaultOrientation, glyphs, _characterFrames, len); free(characters); free(glyphs); // Measure how mush specec will be needed for this attributed string // So we can find minimun frame needed CFRange fitRange; CGSize s = CTFramesetterSuggestFrameSizeWithConstraints(_framesetter, rangeAll, NULL, CGSizeMake(W, MAXFLOAT), &fitRange); _frameRect = CGRectMake(0, 0, s.width, s.height); CGPathRef framePath = CGPathCreateWithRect(_frameRect, NULL); _ctFrame = CTFramesetterCreateFrame(_framesetter, rangeAll, framePath, NULL); CGPathRelease(framePath); // Get the lines in our frame NSArray* lines = (NSArray*)CTFrameGetLines(_ctFrame); _lineCount = [lines count]; // Allocate memory to hold line frames information: if (_lineOrigins != NULL)free(_lineOrigins); _lineOrigins = malloc(sizeof(CGPoint) * _lineCount); if (_lineFrames != NULL)free(_lineFrames); _lineFrames = malloc(sizeof(CGRect) * _lineCount); // Get the origin point of each of the lines CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, 0), _lineOrigins); // Solution borrowew from (but simplified): // https://github.com/twitter/twui/blob/master/lib/Support/CoreText%2BAdditions.m // Loop throught the lines for(CFIndex i = 0; i < _lineCount; ++i) { CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i]; CFRange lineRange = CTLineGetStringRange(line); CFIndex lineStartIndex = lineRange.location; CFIndex lineEndIndex = lineStartIndex + lineRange.length; CGPoint lineOrigin = _lineOrigins[i]; CGFloat ascent, descent, leading; CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading); // If we have more than 1 line, we want to find the real height of the line by measuring the distance between the current line and previous line. If it only 1 line, then we'll guess the line height. BOOL useRealHeight = i < _lineCount - 1; CGFloat neighborLineY = i > 0 ? _lineOrigins[i - 1].y : (_lineCount - 1 > i ? _lineOrigins[i + 1].y : 0.0f); CGFloat lineHeight = ceil(useRealHeight ? abs(neighborLineY - lineOrigin.y) : ascent + descent + leading); _lineFrames[i].origin = lineOrigin; _lineFrames[i].size = CGSizeMake(lineWidth, lineHeight); for (int ic = lineStartIndex; ic < lineEndIndex; ic++) { CGFloat startOffset = CTLineGetOffsetForStringIndex(line, ic, NULL); _characterFrames[ic].origin = CGPointMake(startOffset, lineOrigin.y); } } } #pragma mark - Rendering Text: -(void)renderInContext:(CGContextRef)context contextSize:(CGSize)size{ CGContextSaveGState(context); // Draw Core Text attributes string: CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, CGRectGetHeight(_frameRect)); CGContextScaleCTM(context, 1.0, -1.0); CTFrameDraw(_ctFrame, context); // Draw line and letter frames: CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:0.5].CGColor); CGContextSetLineWidth(context, 1.0); CGContextBeginPath(context); CGContextAddRects(context, _lineFrames, _lineCount); CGContextClosePath(context); CGContextStrokePath(context); CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5].CGColor); CGContextBeginPath(context); CGContextAddRects(context, _characterFrames, _attributedString.string.length); CGContextClosePath(context); CGContextStrokePath(context); CGContextRestoreGState(context); } 
+42
ios iphone ipad core-text glyph
Jan 29 '14 at 22:03
source share
1 answer

You did an impressive amount of work on your subject and were so close in your discretion. The problem you are facing comes from this line of code where you set the bounding fields for each frame:

 _characterFrames[ic].origin = CGPointMake(startOffset, lineOrigin.y); 

The problem is that you are redefining any offset that the frame already had.

If you want to comment on this line, you will see that all frames were located more or less in one place, but you will also see that they are not located in the same place. Some of them are located more to the left or right, and some up or down. This means that glyph frames have their own position.

enter image description here

The solution to your problem is to take into account the current position of the frames when moving them to the right place on the lines. You can do this by adding to x and y separately:

 _characterFrames[ic].origin.x += startOffset; _characterFrames[ic].origin.y += lineOrigin.y; 

or by offsetting the rectangle:

 _characterFrames[ic] = CGRectOffset(_characterFrames[ic], startOffset, lineOrigin.y); 

The bounding rectangles will now have the correct positions:

enter image description here

and you should see that it works for some more extreme fonts there

enter image description here

+50
Feb 01 '14 at 11:53
source share



All Articles