CTLine or NSAttributedString -- get image bounds without a graphics context?

3.1k views Asked by At

Is this possible? Basically, I have a bunch of NSAttributedString objects and corresponding CTLine objects. I want to get the image bounds before the drawRect stage. So at this point, there is nothing to draw into. I will then use these image bounds to decide exactly what I need to create for drawing.

EDIT: Another measurement of the size would probably work just fine. But calling the deceptively named CTLineGetTypographicBounds function only returns the width. If I pass in addresses of ascent and descent floats, they come back as zero.

EDIT: The given answer works great in MacOS. Can anyone do it in iOS?

4

There are 4 answers

1
Kostub Deshmukh On BEST ANSWER

If you are developing for iOS6+. You can use the following method:

CTLineRef line;
// Create the line...

CGRect bounds = CTLineGetBoundsWithOptions(line, kCTLineBoundsUseGlyphPathBounds);
// use bounds...

This is gives the same bounds as CTLineGetImageBounds() assuming you have no transforms applied in your context, but does not require the context. For iOS 5 and below, you would need to use the method described by Иван.

CTLineGetTypographicBounds() gives me a different width than this function or image bounds. I am not sure why. And the ascent and descent returned are those of the font and not the characters displayed in the CTLineRef.

0
mohsenr On

Bounds returned by CTLineGetTypographicBounds() are not the same as image bounds. As the name, (and Иван's answer) suggests, ascent etc. are defined for the font and won't change based on the string. For example, you would use it if you want to find the correct line height if you have a multiline text, as line height normally should not depend on the exact characters you use.

CTLineGetImageBounds() on the other hand, returns the bounds that exactly fit the image. For example, if you want to draw a box around a single line, this is what you need.

CTLineGetImageBounds() needs a context because there may be text transforms and things like that. If you don't want to worry about that, just use a dummy context. For example:

CTLineRef line;

// create the line...

UIGraphicsBeginImageContext(CGSizeMake(1, 1));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextPosition(context, 0, 0);
CGRect bounds = CTLineGetImageBounds(line, context);
UIGraphicsEndImageContext();

// use bounds...
1
Jonan Gueorguiev On

Yes, you can, but not so easy.

You should, generally use CTLineGetTypographicBounds() which, for me, does return ascent, descent and leading, but a bit messed up - 'ascent' equals the total height (i.e. what should be ascent + descent) and 'descent' is always the maximum descent of the font - no matter if you have descending characters or not.

Other way is to retrieve the CTRun(s) from the line (CTLineGetGlyphRuns), then get the glyphs array (CTRunGetGlyphs or CTRunGetGlyphsPtr) and then using CTFontGetBoundingRectsForGlyphs and CTFontGetAdvancesForGlyphs build up the information you need.

EDIT: I've just found this method: "- (NSRect) boundingRectWithSize:(NSSize)size options:(NSStringDrawingOptions)options" of NSAttributedString which seems to do exactly what is needed.

Hope, this is helpful...

0
Merk On

Another method is to convert the string to glyphs usingCTFontGetGlyphsForCharacters() and then calling CTFontGetBoundingRectsForGlyphs() with the glyph array you get from the first function. The latter function returns "the overall bounding rectangle for the glyph run" so don't worry about having to do processing on the individual bounding rects. If used both these functions successfully in iOS.

If you do this remember the mapping between glyphs and characters is not always one to one, especially when the string has non-English characters.