0
votes

In order to positioning / scaling font glyphs in a custom UIView I need to know some glyph metrics, such as: - ascent (height of the glyph from the base line, such as the part of "g" that stands over the base line) - descent (depth of the glyph from the base line, such as the part of "g" that stands under the base line) - width - kerning - italic correction (the part of the glyph that exceeds its width in italic)

I tried subclassing NSLayoutManager and read those information from drawGlyphs:

override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
    enumerateLineFragments(forGlyphRange: glyphsToShow) {
        (rect, usedRect, textContainer, glyphRange, stop) in
        for i in glyphsToShow.location ..< NSMaxRange(glyphsToShow) {

            if let textContainer = self.textContainer(forGlyphAt: glyphsToShow.location, effectiveRange: nil) {
                var glyphRect = self.boundingRect(forGlyphRange: NSMakeRange(i, 1), in:textContainer)
                glyphRect.origin.x += origin.x;
                glyphRect.origin.y += origin.y;

                /// NOW I HAVE AT LEAST THE BOUNDING BOX
            }
        }
    }
}

but glyphRect has the same exact width/height for every glyph, so it carries the max (height+depth) vertical space and max width for the whole font, which isn't what I need (I need those information for every glyph: I is taller than i and j has depth while E hasn't).

Is it possible to collect this information via TextKit? Are the other font metrics (kerning, italic correction) available?

Thank you for your help, Luca.

1

1 Answers

6
votes

Typically you'd do this with Core Text, not NSLayoutManager, at least for width, ascent and decent. I'll discuss the others below.

Consider an attributed string:

let string = NSAttributedString(string: "squids")

From there we want to break it into "glyph runs." A glyph run is a sequence of glyphs with all the same attributes. (In this case there's only one.) To do that, first make a CTLine, an then ask for the CTRun objects:

let line = CTLineCreateWithAttributedString(string)
let glyphRuns = CTLineGetGlyphRuns(line) as! [CTRun]

Each run will have a font, which is what we need to look up the metrics, and a collection of glyphs. Here's a sketch of the calling code:

for run in glyphRuns {
    let font = run.font!
    let glyphs = run.glyphs()
    let boundingRects = run.boundingRects(for: glyphs, in: font)
    for pair in zip(glyphs, boundingRects) { print(pair) }
}

Of course CTRun doesn't have such a nice interface, so we need to build it as an extension:

extension CTRun {
    var font: CTFont? {
        let attributes = CTRunGetAttributes(self) as! [CFString: Any]
        guard let font = attributes[kCTFontAttributeName] else { return nil }
        return (font as! CTFont)
    }

    func glyphs(in range: Range<Int> = 0..<0) -> [CGGlyph] {
        let count = range.isEmpty ? CTRunGetGlyphCount(self) : range.count
        var glyphs = Array(repeating: CGGlyph(), count: count)
        CTRunGetGlyphs(self, CFRangeMake(range.startIndex, range.count), &glyphs)
        return glyphs
    }

    func boundingRects(for glyphs: [CGGlyph], in font: CTFont) -> [CGRect] {
        var boundingRects = Array(repeating: CGRect(), count: glyphs.count)
        CTFontGetBoundingRectsForGlyphs(font, .default, glyphs, &boundingRects, glyphs.count)
        return boundingRects
    }
}

Keep in mind that these are metrics. They're not the actual bounding box of the drawn glyph. Some fonts draw outside their box (Zapfino is famous for it). If you want the actual image box, then you need CTRunGetImageBounds instead. There is also CTFontGetOpticalBoundsForGlyphs which will give you boxes more useful for lining things up correctly (since glyphs often look better if lined up in ways that do not precisely match how they're drawn).

I assume you're familiar with all this, but for completeness, remember that many things that don't have a "descender" per se still have a descent. For example, in Helvetica, the "s" descends slightly below the baseline (also "d" and many other glyphs with a curved base).

To the other metrics you note, some of these aren't glyph metrics. For example, a single glyph doesn't have its own kerning metric. Kerning is something that's applied to pairs of glyphs.

Similarly, I don't really feel italic correction applies here. In the vast majority of cases, you don't "italic" a font on Cocoa platforms. You choose the italic variant of a font, but that's a completely different font. So you don't apply spacing correction to "italicized Helvetica." You just substitute Helvetica-Oblique, which has its own widths, etc. (There's no Helvetica-Italic in the Cocoa world, but there is a HelveticaNeue-Italic.) I don't know of anywhere in the Cocoa layout system where a "italic correction" is applied. There are some places it would be probably nice, but I can't think of anywhere it actually happens.