How to calculate Frame size to draw CoreText by line
Asked Answered
M

1

8

I am trying to dynamically create book pages based on a long NSAttributedString splitted into pieces.

What I am doing now is using this category for NSAttributedString:

@interface NSAttributedString (Height)
- (CGFloat)boundingHeightForWidth:(CGFloat)inWidth;
@end

@implementation NSAttributedString (Height)

- (CGFloat)boundingHeightForWidth:(CGFloat)inWidth
{
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFMutableAttributedStringRef)self); 
    CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(inWidth, 10000), NULL);
    CFRelease(framesetter);
    return suggestedSize.height ;
}
@end

Since I was drawing the text using a CTFramesetter, this worked fine and the correct height for the box was correctly returned. Unfortunately now I need to draw the text line by line:

-(void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();

// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

NSArray *lines = (__bridge NSArray *)(CTFrameGetLines(ctFrame));

CFIndex lineCount = [lines count];

for(CFIndex idx = 0; idx < lineCount; idx++)
{
    // For each line found from where it starts and it's length
    CTLineRef line = CFArrayGetValueAtIndex((CFArrayRef)lines, idx);
    CFRange lineStringRange = CTLineGetStringRange(line);
    NSRange lineRange = NSMakeRange(lineStringRange.location, lineStringRange.length);
    // Get the line related string 
    NSString* lineString = [displayedString.string substringWithRange:lineRange];
    // Calculate it's range
    NSRange stringRange = NSMakeRange(lineStringRange.location, lineStringRange.length);

    static const unichar softHypen = 0x00AD;
    // Get the last char of the line
    unichar lastChar = [lineString characterAtIndex:stringRange.length-1];
    // Check if it's a soft hyphenation character
    if(softHypen == lastChar) {
        NSMutableAttributedString* lineAttrString = [[displayedString attributedSubstringFromRange:stringRange] mutableCopy];
        NSRange replaceRange = NSMakeRange(stringRange.length-1, 1);
        // Replace it with an hard hyphenation character
        [lineAttrString replaceCharactersInRange:replaceRange withString:@"-"];

        CTLineRef hyphenLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)lineAttrString);
        CTLineRef justifiedLine = CTLineCreateJustifiedLine(hyphenLine, 1.0, self.frame.size.width);

        CGFloat ascent;
        CGFloat descent;
        // Calculate the line height
        CTLineGetTypographicBounds(justifiedLine, &ascent, &descent, NULL);
        // Set the correct position for the line
        CGContextSetTextPosition(context, 0.0, idx*-(ascent + descent)-ascent);
        CTLineDraw(justifiedLine, context);
    }
    else{
        CGFloat ascent;
        CGFloat descent;
        // Calculate the line height
        CTLineGetTypographicBounds(line, &ascent, &descent, nil);
        CGFloat y = idx*-(ascent + descent)-ascent;
        // Set the correct position for the line
        CGContextSetTextPosition(context, 0.0, y);

        CTLineDraw(line, context);
    }
}

This is working almost fine but not always. Sometimes happens this:

Last string cutted out

As you can see the last line is cutted out. Has someone any advice to fix this annoying problem?

Monegasque answered 9/3, 2013 at 11:40 Comment(1)
Perhaps this is of help to you: #6338616 - Seems that there is a discrepancy between how the lineheight that CTFrameSetter suggests and the line height that is contained within the font itself.Cortezcortical
M
5

For some reasons seems that (ascent + descent) isn't the correct height for a line. In fact ascender + 1 + descender is equal to the Point Size.

Simply changing this:

CGContextSetTextPosition(context, 0.0, idx*-(ascent + descent)-ascent);

into this:

CGContextSetTextPosition(context, 0.0, idx*-(ascent - 1 + descent)-ascent);

did the trick. Reference: Cocoanetics article about UIFont. Thanks to jverrijt for the hint!

Monegasque answered 10/3, 2013 at 11:36 Comment(1)
The 1 there is an approximation of the leading. You should get the true value of the leading via CTLineGetTypographicBounds(justifiedLine, &ascent, &descent, &leading); It might also be better to use CTFrameGetLineOrigins if any of your lines are different heights. 🙂Dysteleology

© 2022 - 2024 — McMap. All rights reserved.