CTRunGetImageBounds returning inaccurate results

I am using Core Text to draw some text. I would like to get the various run bounds, but when I call CTRunGetImageBounds, the rect that is returned is the correct size, but in the wrong location. Specifically, the line origin is at the end of the text as a whole.

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    self.transform = CGAffineTransformMakeScale(1.0, -1.0);
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    [[UIColor whiteColor] set];
    CGContextFillRect(context, self.bounds);

    NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString:@"Blue should be underlined."];
    NSRange blueRange = NSMakeRange(0, 4);
    [attrString beginEditing];
    //make all text size 20.0
    [attrString addAttribute:(NSString *)kCTFontAttributeName value:(id)CTFontCreateWithName((CFStringRef)@"Helvetica", 20.0, NULL) range:NSMakeRange(0, [attrString length])];
    //make the text appear in blue
    [attrString addAttribute:(NSString *)kCTForegroundColorAttributeName value:(id)[[UIColor blueColor] CGColor] range:blueRange];
    //next make the text appear with an underline
    [attrString addAttribute:(NSString *)kCTUnderlineStyleAttributeName value:[NSNumber numberWithInt:1] range:blueRange];
    [attrString endEditing];

    CGMutablePathRef path = CGPathCreateMutable();
    CGRect bounds = CGRectMake(10.0, 10.0, 200.0, 200.0);
    [[UIColor redColor] set];
    CGContextFillRect(context, bounds);
    CGPathAddRect(path, NULL, bounds);

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
    CTFrameDraw(frame, context);

    for (id lineObj in (NSArray *)CTFrameGetLines(frame)) {
        CTLineRef line = (CTLineRef)lineObj;
        for (id runObj in (NSArray *)CTLineGetGlyphRuns(line)) {
            CTRunRef run = (CTRunRef)runObj;
            CGRect runBounds = CTRunGetImageBounds(run, context, CFRangeMake(0, 0));
            NSLog(@"bounds: %@", NSStringFromCGRect(runBounds));

            [[UIColor greenColor] set];
            CGContextFillRect(context, runBounds);
        }
    }

    CFRelease(framesetter);
    CFRelease(frame);
    [attrString release];
}

Produces:

  • Show JSON data in a TableView with Swift
  • Shadow effects on ImageView in iOS
  • UIInterfaceOrientation landscape Vs Portrait issue in Xcode 5
  • How can I create a 'share to Facebook button' in a SpriteKit game using swift?
  • How to use AVAssetReader and AVAssetWriter for multiple tracks (audio and video) simultaneously?
  • Cannot instantiate UIView from nib. “warning: could not load any Objective-C class information”
  • results

    2 Solutions Collect From Internet About “CTRunGetImageBounds returning inaccurate results”

    Here is what I came up with. This is completely accurate.

    CTFrameRef frame = [self _frameWithRect:rect];
    
    NSArray *lines = (NSArray *)CTFrameGetLines(frame);
    
    CGPoint origins[[lines count]];//the origins of each line at the baseline
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
    
    NSUInteger lineIndex = 0;
    for (id lineObj in lines) {
        CTLineRef line = (CTLineRef)lineObj;
    
        for (id runObj in (NSArray *)CTLineGetGlyphRuns(line)) {
            CTRunRef run = (CTRunRef)runObj;
            CFRange runRange = CTRunGetStringRange(run);
    
            CGRect runBounds;
    
            CGFloat ascent;//height above the baseline
            CGFloat descent;//height below the baseline
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
    
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            runBounds.origin.x = origins[lineIndex].x + rect.origin.x + xOffset;
            runBounds.origin.y = origins[lineIndex].y + rect.origin.y;
            runBounds.origin.y -= descent;
    
            //do something with runBounds
        }
        lineIndex++;
    }
    

    Before you can use CTRunGetImageBounds() or CTLineDraw() you need to set the starting text position with a call to CGContextSetTextPosition() before drawing or computing each line. The reason is a CTLine has no idea where to start drawing (or computing) on its own, it uses the last text position, so you need to give it an XY starting point. To get the origins for a frame’s array of lines, call CTFrameGetLineOrigins() with an array of points. Then, before drawing or computing each line – set the origin of that line via CGContextSetTextPosition().

    NSArray *lines = (NSArray *)CTFrameGetLines(frame);
    
    CGPoint origins[[lines count]];                              // the origins of each line at the baseline
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); 
    
    int i, lineCount;
    
    lineCount = [lines count];
    
    for (i = 0; i < lineCount; i++) {
     CTLineRef line = (CTLineRef)[lines objectAtIndex:i];
     CGContextSetTextPosition(context, origins[i].x, origins[i].y);
     CTLineDraw(line, context);
     }