Search code examples
objective-cioscore-text

iOS CoreText: CTFrameSetter is making wider lines than the CGPath allows


I'm using CoreText to render my text in a CGPath that is typically a rectangle. The width of the path is 230 pixels. But when I'm rendering my text, CTLineGetTypographicBounds returns widths of 292 pixels instead of the maximum 230.

It all worked fine until I started to use CTRunDelegateCallbacks delegates. I use it because I render images in-place. The callbacks just return the correct width and height of the messages. I don't see what's wrong. It only happens when I add a lot of smileys to the NSAttributedString. I expect that it sets it correctly acros multiple lines but it doesn't.

static void _deallocCallback (void* ref)
{
    [(id)ref release];
}

static CGFloat _ascentCallback (void *ref)
{
    UIImage* smiley = [(NSDictionary*)ref objectForKey:@"smiley"];
    return smiley.size.height; // returns 16
}

static CGFloat _widthCallback (void* ref)
{
    UIImage* smiley = [(NSDictionary*)ref objectForKey:@"smiley"];
    return smiley.size.width; // returns 16
}

- (void)renderText:(NSString*)text inRect:(CGRect)rect
{
    CGMutablePathRef path = createMyPath();
    NSAttributedString* attrString = [self _createAttributedString:text];
    // Create the frame setter and it's frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, NULL);

    // Get the height of the lines
    CFArrayRef lines = CTFrameGetLines(frame);
    CFIndex lineCount = CFArrayGetCount(lines);

    CGFloat ascent, descent, leading;

    // Get the origins of each line
    CGPoint origins[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);

    // Loop through every line
    for (CFIndex i = 0; i < lineCount; i++)
    {
        // Get the typographic bounds of this line
        CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
        textWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        // __this returns values wider than rect in path__
    }
}

- (NSAttributedString*)_createAttributedString:(NSString*)inString
{
    // Create our default font attribute
    NSDictionary* fontAttribute = [NSDictionary dictionaryWithObject:(id)_font forKey:(NSString*)kCTFontAttributeName];

    // Create the attributed string
    NSMutableAttributedString* attributedString = [[NSMutableAttributedString alloc] init];

    CTRunDelegateCallbacks callbacks;
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = _ascentCallback;
    callbacks.getDescent = NULL;
    callbacks.getWidth = _widthCallback;
    callbacks.dealloc = _deallocCallback;

    UIImage* smiley = getSmiley(@"sad.png");

    // Do this multiple times to add a long list of smileys   
    NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:                  smiley, @"smiley", nil];
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, attributes);
    NSDictionary* attributeDelegate = [NSDictionary dictionaryWithObjectsAndKeys:(id)delegate, (NSString *)kCTRunDelegateAttributeName, smiley, @"smiley", nil];

    [attributedString appendAttributedString:[[[NSAttributedString alloc] initWithString:@" " attributes:attributeDelegate] autorelease]];

    [attributedString appendAttributedString:[[[NSAttributedString alloc] initWithString:@" blablabla " attributes:fontAttribute] autorelease]];

    return attributedString;
}

Thanks, Nicolas


Solution

  • I do not consider this a beautiful solution but it seems to fix your problem. The following line in your code is responsible for the text not rendering/wrapping correctly:

    [attributedString appendAttributedString:[[[NSAttributedString alloc] initWithString:@" " attributes:attributeDelegate] autorelease]];
    

    It appears CoreText ignores these spaces that are added to attributedString. You can fix this by simply adding a 'dummy' character that is in fact rendered but overlapped by the image and thus invisible. This character will be seen as a new 'word' by Coretext and should render/wrap just fine.

    For example:

    [attributedString appendAttributedString:[[[NSAttributedString alloc] initWithString:@"•" attributes:attributeDelegate] autorelease]];