Why fonts loaded using CoreText (CTFont) behave different than loaded using AppKit (NSFont)?

I tried to load font using CTFontCreateWithFontDescriptor() and it behaves differently than loaded using NSFont(). Let's say the goal is to load font using CoreText that is exactly the same as using AppKit.

NSTextField

If I use them in NSTextField text offsets and rendering do not match.

let familyName = "Helvetica"
let fontSize: CGFloat = 30
let string = "Hello, World! gÖ,|#"

// Load font using AppKit
let font1 = NSFont(name: familyName, size: fontSize)!

// Load font using CoreText API
let font2 = CTFontCreateWithFontDescriptor(CTFontDescriptorCreateWithAttributes([
    kCTFontFamilyNameAttribute: familyName,
    kCTFontSizeAttribute: fontSize,
] as [CFString : Any] as CFDictionary), 0, nil) as NSFont

// Setup text fields

let field1 = NSTextField()
let field2 = NSTextField()
field1.setAsLabel()
field2.setAsLabel()
field1.font = font1
field2.font = font2
field1.stringValue = string
field2.stringValue = string

NSAttributedString

If I use them in NSAttributedString bounding box height of text is different:

let bounds1 = NSAttributedString(string: string, attributes: [
    .font: font1
]).boundingRect(with: .infinity)

let bounds2 = NSAttributedString(string: string, attributes: [
    .font: font2
]).boundingRect(with: .infinity)

print(bounds1) // Prints: (0.0, -7.0, 252.3486328125, 37.0)
print(bounds2) // Prints: (0.0, -7.0, 252.3486328125, 31.0)

Investigation

I compared fonts, font attributes, traits, feature settings, even binary font tables but I didn't find any difference. It seems like there is something inside that alters behaviour.

I found that if I add .usesDeviceMetrics option to NSAttributedString bounds getter, it returns the same result but also completely different frame.

let bounds1 = NSAttributedString(string: "Hello, World! gÖ,|#", attributes: [.font: font1]).boundingRect(with: .infinity, options: [.usesDeviceMetrics])
let bounds2 = NSAttributedString(string: "Hello, World! gÖ,|#", attributes: [.font: font2]).boundingRect(with: .infinity, options: [.usesDeviceMetrics])

print(bounds1) // Prints: (2.3583984375, -6.6357421875, 249.9609375, 33.837890625)
print(bounds2) // Prints: (2.3583984375, -6.6357421875, 249.9609375, 33.837890625)

Question

How to load a font using CoreText in a way that NSFont does?

Minor question: Why does NSTextField set it's frame origin to -2.0?


Just extensions I used in code above:

extension CGSize {
    public static var infinity: Self {
        .init(width: CGFloat.infinity, height: CGFloat.infinity)
    }
}

extension NSTextField {
    func setAsLabel() {
        translatesAutoresizingMaskIntoConstraints = false
        isBezeled = false
        isBordered = false
        isEditable = false
        isSelectable = false
        drawsBackground = false
    }
}

Accepted Reply

After months I found the reason. There is difference in behaviour if app is compiled for deployment target macOS 10.13 and macOS 10.14+. Seems like there is a bug in macOS 10.13 and loading font is strange. I compared NSTextField with 2 fonts loaded as NSFont and CTFont.

(Again. Does not depend on OS where app runs but what deployment target it was compiled)

I also have custom implementation of label that uses TextKit objects NSTextLayoutManager, NSTextContainer and NSTextStorage. Text is rendered using layout manager drawGlyphs(forGlyphRange:at:) and geometry is also provided by these objects.

Deployment target macOS 10.13

Different handling NSFont and CTFont by NSTextField and by NSTextLayoutManager. All results are different. CTFont seems to be the closest one in comparison to new OS.

Deployment target macOS 10.14

All approaches give the same result

Replies

After months I found the reason. There is difference in behaviour if app is compiled for deployment target macOS 10.13 and macOS 10.14+. Seems like there is a bug in macOS 10.13 and loading font is strange. I compared NSTextField with 2 fonts loaded as NSFont and CTFont.

(Again. Does not depend on OS where app runs but what deployment target it was compiled)

I also have custom implementation of label that uses TextKit objects NSTextLayoutManager, NSTextContainer and NSTextStorage. Text is rendered using layout manager drawGlyphs(forGlyphRange:at:) and geometry is also provided by these objects.

Deployment target macOS 10.13

Different handling NSFont and CTFont by NSTextField and by NSTextLayoutManager. All results are different. CTFont seems to be the closest one in comparison to new OS.

Deployment target macOS 10.14

All approaches give the same result

I forgot to say that used font above is Inter there is what Helvetica looks like:

Deployment target macOS 10.13

Bad handling of CTFont by NSTextField.

Deployment target macOS 10.14

Also bad handling of CTFont by NSTextField.

Seems like it also depends on specific font. Each font can behave differently. 🤦‍♂️

So I rather prefer my custom label implementation but it's hard to sync vertical alignment with NSTextField that is right next to it. Anyway this hidden font undocumented bug is insidious. I spent a lot of time to figure out what is going on there.