Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CTRunGetImageBounds returning inaccurate results

Tags:

ios

core-text

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:

results

like image 596
David Beck Avatar asked Dec 06 '22 22:12

David Beck


2 Answers

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++;
}
like image 61
David Beck Avatar answered Dec 27 '22 07:12

David Beck


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);
 }
like image 35
Chris Walken Avatar answered Dec 27 '22 07:12

Chris Walken