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

46 views Asked by At

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

NSTextField result preview

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
    }
}
0

There are 0 answers