0

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.

Luca
  • 303
  • 2
  • 13

1 Answers1

7

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you so much Rob for your in-depth answer! I'm fascinated by text layout engines in general and math equations layout in particular, so I'm trying my best to understand how TeX lays out math. This is the main reason why I'm looking also for the italic correction. I will try your solution as soon as possible. In the meanwhile I discovered that many implementations use some kind of precomputed font metrics stored in files such as TeX `.tfm` file. So an alternative approach could be to write the Swift code needed to parse those files. Thank you again for your answer! – Luca Nov 14 '19 at 21:02
  • 1
    TeX is quite advanced and specialized for this problem. Core Text is much more focused on laying out *text* (i.e. words, intended for humans to read as labels and sentences). If you're interested mathematical layout, I would definitely keep digging into how TeX does things, and translating those to Core Text could be a very interesting and fruitful project. A huge amount of work has gone into TeX. – Rob Napier Nov 15 '19 at 15:13
  • I know this is a challenging task (at least) and I don't expect to be able to port the whole TeX math layout engine in Swift. Nevertheless I approach this task as a way to learn new things and improve my knowledge of Swift. I'm following this paper https://people.eecs.berkeley.edu/~fateman/temp/neuform.pdf that presents the whole algorithm in a "poor human understandable" format. I know there is already an ObjC library, https://github.com/kostub/iosMath, that does a great job to render math equations, but I'm here to learn. Your explanations have been valuable, I will make good use of them! – Luca Nov 15 '19 at 18:01