Question

I have a NSAttributedString, a rect to draw the whole string, I want to get the rect of the last character just like the image below, how to get that using Core Text ?

Get the rect of last char

Was it helpful?

Solution

Specifically using CoreText, here's a function that should work (with a few comments in the code explaining what's going on):

- (CGRect)lastCharacterRectForAttributedString:(NSAttributedString *)attributedString drawingRect:(CGRect)drawingRect
{
    // Start by creating a CTFrameRef using the attributed string and rect.
    CTFrameRef textFrame = NULL;
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attributedString));
    CGPathRef drawingPath = CGPathCreateWithRect(drawingRect, NULL);
    textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attributedString length]), drawingPath, NULL);
    CFRelease(framesetter);
    CFRelease(drawingPath);

    // Line origins can be obtained from the CTFrameRef. Get the final one.
    CGPoint finalLineOrigin;
    CFArrayRef lines = CTFrameGetLines(textFrame);
    if (CFArrayGetCount(lines) == 0) { // Safety check
        CFRelease(textFrame);
        return CGRectNull;
    }
    const CFIndex finalLineIdx = CFArrayGetCount(lines) - 1;
    CTFrameGetLineOrigins(textFrame, CFRangeMake(finalLineIdx, 1), &finalLineOrigin);

    // Get the glyph runs from the final line. Get the last glyph position from the final run.
    CGPoint glyphPosition;
    CFArrayRef runs = CTLineGetGlyphRuns(CFArrayGetValueAtIndex(lines, finalLineIdx));
    if (CFArrayGetCount(runs) == 0) { // Safety check
        CFRelease(textFrame);
        return CGRectNull;
    }
    CTRunRef finalRun = CFArrayGetValueAtIndex(runs, CFArrayGetCount(runs) - 1);
    if (CTRunGetGlyphCount(finalRun) == 0) { // Safety check
        CFRelease(textFrame);
        return CGRectNull;
    }
    const CFIndex lastGlyphIdx = CTRunGetGlyphCount(finalRun) - 1;
    CTRunGetPositions(finalRun, CFRangeMake(lastGlyphIdx, 1), &glyphPosition);

    // The bounding box of the glyph itself is extracted from the font.
    CGRect glyphBounds;
    CFDictionaryRef runAttributes = CTRunGetAttributes(finalRun);
    CTFontRef font = CFDictionaryGetValue(runAttributes, NSFontAttributeName);
    CGGlyph glyph;
    CTRunGetGlyphs(finalRun, CFRangeMake(lastGlyphIdx, 1), &glyph);
    CTFontGetBoundingRectsForGlyphs(font, kCTFontDefaultOrientation, &glyph, &glyphBounds, 1);

    // Option 1 - The rect you've drawn in your question isn't tight to the final character; it looks approximately the height of the line. If that's what you're after:
    CGRect lineBounds = CTLineGetBoundsWithOptions(CFArrayGetValueAtIndex(lines, finalLineIdx), 0);
    CGRect desiredRect = CGRectMake(
                                    CGRectGetMinX(drawingRect) + finalLineOrigin.x + glyphPosition.x + CGRectGetMinX(glyphBounds),
                                    CGRectGetMinY(drawingRect) + (CGRectGetHeight(drawingRect) - (finalLineOrigin.y + CGRectGetMaxY(lineBounds))),
                                    CGRectGetWidth(glyphBounds),
                                    CGRectGetHeight(lineBounds)
                                    );
    // Option 2 - If you want a rect that closely bounds the final character, use this:
    /*
    CGRect desiredRect = CGRectMake(
                                    CGRectGetMinX(drawingRect) + finalLineOrigin.x + glyphPosition.x + CGRectGetMinX(glyphBounds),
                                    CGRectGetMinY(drawingRect) + (CGRectGetHeight(drawingRect) - (finalLineOrigin.y + glyphPosition.y + CGRectGetMaxY(glyphBounds))),
                                    CGRectGetWidth(glyphBounds),
                                    CGRectGetHeight(glyphBounds)
                                    );
    */

    CFRelease(textFrame);

    return desiredRect;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top