Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Text: Simplest accurate means of getting text bounds?

Question:

What is the simplest accurate means of getting the bounds for a given line of text, using Core Text?

Problem:

I have tried multiple techniques, but am getting inconsistent results.

Context:

I have been trying to figure out how to find the actual bounds of what is for now, a single line of text.

The Core Text documentation is at best incomplete on what the frame, line, and run level routines return, stating that they return the "actual" bounds, when they in fact disagree (at least on the frame level), and don't return the actual bounds for the specific string, but appear to return a size larger than the font size would predict.

However, when I get the bounds on individual glyphs, I do see differences between different characters, suggesting to me that this is the only accurate method (iterating through all glyphs, finding the effective union of them all).

I have written some tests in a playground file, am I missing something obvious? My final goal (that partly / mostly works) is to have a custom CALayer that takes an attributed string, and scales it to fill the entire bounds. This then plays nicely with auto layout, in that I can create a label that scales with my layout, as desired.

My playground snippet is below, note the values generated on the right column.

// Playground - noun: a place where people can play

import UIKit
import CoreText
import QuartzCore

print("start")



let f = UIFont.systemFontOfSize(12)
let t = NSAttributedString(string: "A", attributes: [NSFontAttributeName:f])
let line = CTLineCreateWithAttributedString(t)
var ascent:CGFloat = 0, descent:CGFloat = 0, leading:CGFloat = 0
CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
ascent
descent
ascent + descent
leading

let boundingSize = CGSizeMake(CGFloat.max, CGFloat.max)
let fs = CTFramesetterCreateWithAttributedString(t)
let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(fs, CFRangeMake(0, t.length),
    nil, boundingSize, nil)
frameSize
let lineWidth = frameSize.width
let lineHeight = frameSize.height


// for each run
let runs = CTLineGetGlyphRuns(line) as [AnyObject] as! [CTRun]
for (runIndex, run) in runs.enumerate() {

    // get font
    let attributes = CTRunGetAttributes(run) as NSDictionary as! [String:AnyObject]
    let runFont:CTFont = attributes[kCTFontAttributeName as String] as! CTFont

    // get glyphs for current run
    let glyphCount = CTRunGetGlyphCount(run)
    let range = CFRangeMake(0, glyphCount)
    var glyphs = Array<CGGlyph>(count: glyphCount, repeatedValue: CGGlyph())
    var positions = [CGPoint](count: glyphCount, repeatedValue: CGPointZero)
    CTRunGetGlyphs(run, range, &glyphs)
    CTRunGetPositions(run, range, &positions)

    let runWidth = CTRunGetTypographicBounds(run, range, &ascent, &descent, &leading)
    runWidth

    ascent
    descent
    leading

    // for each glyph in run
    for (glyphInRun, var glyph) in glyphs.enumerate() {

        let glyphFrame = withUnsafePointer(&glyph) { pointer -> CGRect in
            return CTFontGetBoundingRectsForGlyphs(runFont, .Default, pointer, nil, 1) }
        glyphFrame.size
    }
}



print("done")

Thanks!

like image 924
Chris Conover Avatar asked Mar 14 '23 06:03

Chris Conover


1 Answers

You're using CTRunGetTypographicBounds. Typographic bounds means the bounds used for typesetting, so the ascent, descent, and advance are based on the font metrics, not on the actual appearance of the glyphs in the run.

It sounds like you want CTRunGetImageBounds, which returns a CGRect that “tightly encloses the paths of the run's glyphs”. Note that you can pass nil for the context argument (in my testing).

like image 79
rob mayoff Avatar answered Mar 16 '23 20:03

rob mayoff