How to get the real height of text drawn on a CTFrame
Asked Answered
T

4

6

I have a certain amount of text that fill some CTFrame (more than one). To create all frames (one for each page), I'm filling one frame, getting the text that didn't fitted the frame using CTFrameGetVisibleStringRange and repeating this process until all text is processed.

On all frames, except the last, the text occupies the same height of page. On last frame I'd like to know the real height the text occupies, to know where I could start drawing more text.

Is there any way to do this?

UPDATE

As requested on comments, here's my solution using @omz 's suggestion:

I'm using ARC on my project:

CTFrameRef locCTFrame = (__bridge CTFrameRef)ctFrame;

//Save CTLines
lines = (NSArray *) ((__bridge id)CTFrameGetLines(locCTFrame));

//Get line origins
CGPoint lOrigins[MAXLINESPERPAGE];
CTFrameGetLineOrigins(locCTFrame, CFRangeMake(0, 0), lOrigins);
CGFloat colHeight = self.frame.size.height;

//Save the amount of the height used by text
percentFull = ((colHeight - lOrigins[[lines count] - 1].y) / colHeight);
Trujillo answered 4/12, 2011 at 18:15 Comment(0)
S
3

You could either get the line origin of the last line in the frame with CTFrameGetLineOrigins or use the CTFramesetterSuggestFrameSizeWithConstraints function to get the size of a rectangular frame for a given range. The latter wouldn't work if you use non-rectangular paths for setting the actual frames though.

Sayre answered 4/12, 2011 at 18:22 Comment(1)
Thanks @Sayre I think I'll use the first one, as I saw on another answer that CTFramesetterSuggestFrameSizeWithConstraints some times returns an incorrect value.Trujillo
G
11
+ (CGSize)measureFrame:(CTFrameRef)frame
{
    // 1. measure width
    CFArrayRef  lines       = CTFrameGetLines(frame);
    CFIndex     numLines    = CFArrayGetCount(lines);
    CGFloat     maxWidth    = 0;

    for(CFIndex index = 0; index < numLines; index++)
    {
        CTLineRef   line = (CTLineRef) CFArrayGetValueAtIndex(lines, index);
        CGFloat     ascent, descent, leading, width;

        width = CTLineGetTypographicBounds(line, &ascent,  &descent, &leading);

        if(width > maxWidth)
            maxWidth = width;
    }

    // 2. measure height
    CGFloat ascent, descent, leading;

    CTLineGetTypographicBounds((CTLineRef) CFArrayGetValueAtIndex(lines, 0), &ascent,  &descent, &leading);
    CGFloat firstLineHeight = ascent + descent + leading;

    CTLineGetTypographicBounds((CTLineRef) CFArrayGetValueAtIndex(lines, numLines - 1), &ascent,  &descent, &leading);
    CGFloat lastLineHeight  = ascent + descent + leading;

    CGPoint firstLineOrigin;
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 1), &firstLineOrigin);

    CGPoint lastLineOrigin;
    CTFrameGetLineOrigins(frame, CFRangeMake(numLines - 1, 1), &lastLineOrigin);

    CGFloat textHeight = ABS(firstLineOrigin.y - lastLineOrigin.y) + firstLineHeight + lastLineHeight;

    return CGSizeMake(maxWidth, textHeight);
}
Gullett answered 15/10, 2012 at 11:32 Comment(4)
Using the CTFrameGetLineOrigins as suggested by @omz, I could get the last line's origin and discover the height used, including blank lines.Trujillo
I tried to post the code here but became unreadable. I'll edit the question.Trujillo
Of all the solutions for getting the height of a complex attributed string this has been the closest for me so far.Homesteader
This has generally worked well for me, except that I'm having issues with the height. If I'm understanding correctly, the textHeight calculation is actually too large. Take the one line case - firstLineHeight == lastLineHeight and firstLineOrigin.y == lastLineOrigin.y. Then textHeight will be twice the line height, when you actually just want the line height, correct?Gracielagracile
S
3

You could either get the line origin of the last line in the frame with CTFrameGetLineOrigins or use the CTFramesetterSuggestFrameSizeWithConstraints function to get the size of a rectangular frame for a given range. The latter wouldn't work if you use non-rectangular paths for setting the actual frames though.

Sayre answered 4/12, 2011 at 18:22 Comment(1)
Thanks @Sayre I think I'll use the first one, as I saw on another answer that CTFramesetterSuggestFrameSizeWithConstraints some times returns an incorrect value.Trujillo
D
0

Use CTLineGetTypographicBounds.

Domino answered 4/12, 2011 at 18:19 Comment(1)
Do you have an example of how that will help answer the question?Circumscribe
K
0

I think user1021430 is correct in saying that the height is not correctly calculated.

To get the correct height, you wan to get the top of the first line (origin + first ascent) and the bottom of the last line (origin - descent), and then subtract the two come up with the actual height.

CGSize
MeasureTextWithinFrame(
    CTFrameRef      frame)
{
    CGSize textSize = CGSizeMake(0.0f, 0.0f);
    
    CFArrayRef  lines       = CTFrameGetLines(frame);
    CFIndex     numLines    = CFArrayGetCount(lines);

    // if there is at least one line
    if (numLines > 0) {
    
        // measure width
        for (CFIndex index = 0; index < numLines; index++)  {
            CTLineRef line = (CTLineRef) CFArrayGetValueAtIndex(lines, index);
            CGFloat ascent, descent, leading, width;
            width = CTLineGetTypographicBounds(line, &ascent,  &descent, &leading);
            if (width > textSize.width)
                textSize.width = width;
        }
    
        // measure height
        CGFloat firstAscent, firstDescent, firstLeading;
        CTLineGetTypographicBounds((CTLineRef) CFArrayGetValueAtIndex(lines, 0), &firstAscent,  &firstDescent, &firstLeading);

        CGPoint firstLineOrigin;
        CTFrameGetLineOrigins(frame, CFRangeMake(0, 1), &firstLineOrigin);

        CGFloat lastAscent, lastDescent, lastLeading;
        CTLineGetTypographicBounds((CTLineRef) CFArrayGetValueAtIndex(lines, numLines - 1), &lastAscent,  &lastDescent, &lastLeading);
        
        CGPoint lastLineOrigin;
        CTFrameGetLineOrigins(frame, CFRangeMake(numLines - 1, 1), &lastLineOrigin);
        
        float top = firstLineOrigin.y + firstAscent;
        float bottom = lastLineOrigin.y - lastDescent;
        textSize.height = ABS(top - bottom);
    }
    
    return textSize;
}
Konyn answered 18/6, 2021 at 16:24 Comment(1)
I haven't tested this code since 6 years. It may have changed since then. I'll check and update the answer.Trujillo

© 2022 - 2024 — McMap. All rights reserved.